From f8f83ce4d4efcdf49ceb247e3e4674a971032314 Mon Sep 17 00:00:00 2001 From: 3wc <3wc@doesthisthing.work> Date: Sun, 29 Oct 2023 09:28:25 +0000 Subject: [PATCH] Finish converting CLI commands to use peewee --- hamstertools/__init__.py | 321 +++++++++++++++++---------------------- hamstertools/db.py | 5 +- 2 files changed, 142 insertions(+), 184 deletions(-) diff --git a/hamstertools/__init__.py b/hamstertools/__init__.py index 60e4785..758d2e6 100755 --- a/hamstertools/__init__.py +++ b/hamstertools/__init__.py @@ -11,7 +11,7 @@ from peewee import fn, JOIN from .db import db, HamsterCategory, HamsterActivity, HamsterFact, KimaiCustomer, KimaiProject, KimaiActivity, HamsterKimaiMapping -# HAMSTER_DIR = Path.home() / '.local/share/hamster' +HAMSTER_DIR = Path.home() / '.local/share/hamster' # HAMSTER_FILE = HAMSTER_DIR / 'hamster.db' HAMSTER_FILE = 'hamster-testing.db' @@ -50,7 +50,7 @@ def delete_categories(ids): categories = HamsterCategory.select( HamsterCategory, fn.Count(HamsterActivity.id).alias("activities_count") - ).join(HamsterActivity, JOIN.LEFT_OUTER).group_by(HamsterActivity).where(HamsterCategory.id.in_(ids)) + ).join(HamsterActivity, JOIN.LEFT_OUTER).group_by(HamsterCategory).where(HamsterCategory.id.in_(ids)) for c in categories: click.echo(f'@{c.id}: {c.name} ({c.activities_count} activities)') @@ -68,63 +68,58 @@ def delete_categories(ids): def rename_category(id_, name): """ Rename a category """ - r = get_categories((id_,))[0] + category = HamsterCategory.get(id=id_) - click.echo('Renaming @{0[0]}: {0[1]} to "{1}"'.format(r, name)) + click.echo(f'Renaming @{category.id}: {category.name} to "{name}"') - sql = 'UPDATE categories SET name = ? WHERE id = ?' - - c.execute(sql, (name, r[0])) - conn.commit() + category.name = name + category.save() @categories.command('activities') @click.argument('ids', nargs=-1) def list_category_activities(ids): - """ Show activities for categories specified by IDS """ - sql = ''' - SELECT - activities.id, activities.name, categories.name - FROM - activities - LEFT JOIN - categories - ON - activities.category_id = categories.id - WHERE - categories.id IN ({seq}) - '''.format( - seq=','.join(['?'] * len(ids)) - ) + """ Show activities for categories specified by ids """ - results = c.execute(sql, ids) + activities = HamsterActivity.select( + HamsterActivity, + HamsterCategory.name + ).join(HamsterCategory, JOIN.LEFT_OUTER).where(HamsterCategory.id.in_(ids)) + + for a in activities: + click.echo(f'@{a.id}: {a.category.name} » {a.name}') - for r in results: - click.echo('@{0[0]}: {0[2]} » {0[1]}'.format(r)) @categories.command('tidy') def tidy_categories(): """ Remove categories with no activities """ - sql = 'SELECT categories.id, categories.name FROM categories LEFT JOIN activities ON categories.id = activities.category_id WHERE activities.id IS NULL' - categories = c.execute(sql).fetchall() + # Create a subquery to calculate the count of activities per category + subquery = ( + HamsterCategory + .select(HamsterCategory, fn.COUNT(HamsterActivity.id).alias('activities_count')) + .join(HamsterActivity, JOIN.LEFT_OUTER) + .group_by(HamsterCategory) + .alias('subquery') + ) - click.echo('Found {0} empty categories:'.format(len(categories))) + # Use the subquery to filter categories where activities_count is 0 + categories = ( + HamsterCategory + .select() + .join(subquery, on=(HamsterCategory.id == subquery.c.id)) + .where(subquery.c.activities_count == 0) + ) + + click.echo('Found {0} empty categories:'.format(categories.count())) for cat in categories: - click.echo('@{0[0]}: {0[1]}'.format(cat)) + click.echo(f'@{cat.id}: {cat.name}') click.confirm('Do you want to continue?', abort=True) - sql = 'DELETE FROM categories ' - - sql = sql + 'WHERE id IN ({seq})'.format( - seq=','.join(['?'] * len(categories)) - ) - - c.execute(sql, [cat[0] for cat in categories]) - conn.commit() + [cat.delete_instance() for cat in categories] @cli.group() @@ -138,18 +133,31 @@ def activities(): def list_activities(search, csv_output): """ List / search activities """ - results = get_activities(search=search) + activities = HamsterActivity.select( + HamsterActivity, + HamsterCategory + ).join(HamsterCategory, JOIN.LEFT_OUTER).order_by( + HamsterCategory.name, + HamsterActivity.name + ) - results.sort(key=lambda t: (t[2], t[1])) + if search is not None: + activities = activities.where(HamsterActivity.name.contains(search)) if csv_output: csv_writer = csv.writer(sys.stdout) - for r in results: + for a in activities: + category_name = a.category.name if a.category_id != -1 else "" if csv_output: - csv_writer.writerow([r[3], r[2], r[0], r[1]]) + csv_writer.writerow([ + a.category_id, + category_name, + a.id, + a.name + ]) else: - click.echo('@{0[3]}: {0[2]} » {0[0]}: {0[1]}'.format(r)) + click.echo(f'@{a.category_id}: {category_name} » {a.id}: {a.name}') @activities.command('delete') @@ -157,25 +165,25 @@ def list_activities(search, csv_output): def delete_activities(ids): """ Delete activities specified by IDS """ - results = get_activities(ids) + activities = HamsterActivity.select( + HamsterActivity, + HamsterCategory.name, + fn.Count(HamsterFact.id).alias("facts_count") + ).join(HamsterCategory, + JOIN.LEFT_OUTER).switch(HamsterActivity).join(HamsterFact, + JOIN.LEFT_OUTER).group_by(HamsterActivity).where( + HamsterActivity.id.in_(ids) + ) click.secho('Deleting:', fg='red') - for r in results: - sql = "SELECT COUNT(id) FROM facts WHERE activity_id = ?" - count = c.execute(sql, (r[0],)).fetchone()[0] - click.echo('@{0[0]}: {0[2]} » {0[1]} ({1} facts)'.format(r, count)) + for a in activities: + category_name = a.category.name if a.category_id != -1 else "" + click.echo(f'@{a.id}: {category_name} » {a.name} ({a.facts_count} facts)') click.confirm('Do you want to continue?', abort=True) - sql = 'DELETE FROM activities ' - - sql = sql + 'WHERE id IN ({seq})'.format( - seq=','.join(['?'] * len(ids)) - ) - - c.execute(sql, ids) - conn.commit() + [a.delete_instance() for a in activities] click.secho('Deleted {0} activities'.format(len(ids)), fg='green') @@ -185,29 +193,20 @@ def delete_activities(ids): @click.argument('ids', nargs=-1) def move(category_id, ids): """ Move activities to another category """ - category = get_categories((category_id,))[0] - results = get_activities(ids) + category = HamsterCategory.get(id=category_id) + activities = HamsterActivity.select().where(HamsterActivity.id.in_(ids)) - click.secho('Moving to "@{0[0]}: {0[1]}":'.format(category), fg='green') + click.secho(f'Moving to "@{category.id}: {category.name}":', fg='green') - for r in results: - click.secho('@{0[3]}: {0[2]} » @{0[0]}: {0[1]}'.format(r), fg='blue') + for a in activities: + category_name = a.category.name if a.category_id != -1 else "" + click.secho(f'@{a.category_id}: {category_name} » @{a.id}: {a.name}', fg='blue') click.confirm('Do you want to continue?', abort=True) - sql = ''' - UPDATE - activities - SET - category_id = ? - ''' - - sql = sql + 'WHERE id IN ({seq})'.format( - seq=','.join(['?'] * len(ids)) - ) - - c.execute(sql, (category[0], *ids)) - conn.commit() + for a in activities: + a.category = category + a.save() click.secho('Moved {0} activities'.format(len(ids)), fg='green') @@ -216,32 +215,15 @@ def move(category_id, ids): @click.argument('ids', nargs=-1) def list_facts(ids): """ Show facts for activities """ + activities = HamsterActivity.select().where(HamsterActivity.id.in_(ids)) - results = get_activities(ids) - - for r in results: + for a in activities: click.secho( - '@{0[0]}: {0[1]}'.format(r), fg='green' + f'@{a.id}: {a.name}', fg='green' ) - sql = ''' - SELECT - start_time, - activities.name - FROM - facts - LEFT JOIN - activities - ON - facts.activity_id = activities.id - WHERE - activities.id = ? - ''' - - results = c.execute(sql, (r[0],)) - - for r in results: - click.secho('@{0[0]}, {0[1]}'.format(r), fg='blue') + for f in a.facts: + click.secho(f'@{f.id}, {f.start_time}', fg='blue') @activities.command() @@ -249,82 +231,52 @@ def list_facts(ids): @click.argument('to_id') def move_facts(from_id, to_id): """ Move facts from one activity to another """ - from_activity = get_activities((from_id,))[0] - to_activity = get_activities((to_id,))[0] + from_activity = HamsterActivity.get(id=from_id) + to_activity = HamsterActivity.get(id=to_id) + + from_category_name = from_activity.category.name if from_activity.category_id != -1 else "" + to_category_name = to_activity.category.name if to_activity.category_id != -1 else "" click.secho( - 'Moving facts from "@{0[2]} » @{0[0]}: {0[1]}" to "@{1[2]} » @{1[0]}: {1[1]}"'.format( - from_activity, to_activity - ), fg='green' + f'Moving facts from "{from_category_name} » @{from_activity.id}: {from_activity.name}" to "@{to_category_name} » @{to_activity.id}: {to_activity.name}"', fg='green' ) - sql = ''' - SELECT - start_time, - activities.name - FROM - facts - LEFT JOIN - activities - ON - facts.activity_id = activities.id - WHERE - activities.id = ? - ''' - - results = c.execute(sql, (from_id,)) - - for r in results: - click.secho('@{0[0]}, {0[1]}'.format(r), fg='blue') + for f in from_activity.facts: + click.secho(f'@{f.id}, {f.start_time}', fg='blue') click.confirm('Do you want to continue?', abort=True) - c.execute( - 'UPDATE facts SET activity_id = ? WHERE activity_id = ?', - (to_id, from_id) - ) + count = HamsterFact.update(activity_id=to_activity.id).where(HamsterFact.activity == from_activity).execute() - conn.commit() - - click.secho('Moved {0} facts'.format(results.rowcount), fg='green') + click.secho('Moved {0} facts'.format(count), fg='green') click.confirm( - 'Would you like to delete @{0[2]} » @{0[0]}: {0[1]}?'.format( - from_activity), + f'Would you like to delete "{from_category_name} » @{from_activity.id}: {from_activity.name}?', abort=True ) - delete_activities((from_id,)) + from_activity.delete_instance() @activities.command() def find_duplicates(): """ Show activities which are not unique in their categories """ - sql = ''' - SELECT - categories.id, - categories.name, - activities.id, - activities.name, - COUNT(activities.id) c - FROM - activities - LEFT JOIN - categories - ON - activities.category_id = categories.id - GROUP BY - activities.name, - activities.category_id - HAVING c > 1 - ''' + non_unique_activities = ( + HamsterActivity + .select( + HamsterActivity, + HamsterCategory.id, + fn.COALESCE(HamsterCategory.name, 'None').alias("category_name") + ) + .join(HamsterCategory, JOIN.LEFT_OUTER) + .group_by(HamsterActivity.category_id, HamsterActivity.name) + .having(fn.COUNT(HamsterActivity.id) > 1) + ) - results = c.execute(sql) - - for r in results: + for activity in non_unique_activities: click.secho( - '@{0[0]}: {0[1]} » @{0[2]}: {0[3]} ({0[4]})'.format(r), fg='blue') + f"@{activity.category_id}: {activity.category_name} » @{activity.id}: {activity.name}", fg='blue') @cli.group() @@ -353,11 +305,15 @@ def _get_kimai_mapping_file(path, category_search=None): 'TO Note' ]) - results = get_activities(category_search=category_search) + activities = HamsterActivity.select( + HamsterActivity, + HamsterCategory + ).join(HamsterCategory, JOIN.LEFT_OUTER) - for r in results: + for a in activities: mapping_writer.writerow([ - r[2], r[1] + a.category.name if a.category_id != -1 else "", + a.name ]) mapping_file.close() @@ -534,6 +490,12 @@ def _import(username, mapping_path=None, output=None, category_search=None, afte args = [] + facts = HamsterFact.select( + HamsterFact, + HamsterActivity, + HamsterCategory + ).join(HamsterActivity).join(HamsterCategory, JOIN.LEFT_OUTER) + sql = ''' SELECT facts.id, facts.start_time, facts.end_time, facts.description, @@ -547,20 +509,10 @@ def _import(username, mapping_path=None, output=None, category_search=None, afte categories ON activities.category_id = categories.id ''' if category_search is not None: - sql = sql + " WHERE categories.name LIKE ?" - category_search = '%{0}%'.format(category_search) - args.append(category_search) + facts = facts.where(HamsterCategory.name.contains(category_search)) if after is not None: - if category_search is not None: - sql = sql + ' AND ' - else: - sql = sql + ' WHERE ' - sql = sql + f"DATE(facts.start_time) > DATE(?)" - args.append(after) - - results = c.execute(sql, args) - results = c.fetchall() + facts = facts.where(HamsterFact.start_time >= after) if not show_missing: output_writer.writerow([ @@ -581,13 +533,13 @@ def _import(username, mapping_path=None, output=None, category_search=None, afte "InternalRate" ]) - for fact in results: - k = '{0}:{1}'.format(fact[6], fact[5]) + for fact in facts: + k = f'{fact.activity.category.name}:{fact.activity.name}' try: mapping_ = mapping[k] except KeyError: if show_missing: - output_writer.writerow([fact[6], fact[5]]) + output_writer.writerow([fact.activity.category.name, fact.activity.name]) click.secho( "Can't find mapping for '{0}', skipping".format(k), fg='yellow') continue @@ -595,19 +547,16 @@ def _import(username, mapping_path=None, output=None, category_search=None, afte if show_missing: continue - if fact[1] is None or fact[2] is None: - click.secho("Missing duration data '{0}-{1}', skipping".format( - fact[1], - fact[2] - ), fg='yellow') + if fact.start_time is None or fact.end_time is None: + click.secho(f"Missing duration data '{fact.start_time}-{fact.end_time}', skipping", fg='yellow') continue if len(mapping_) < 5: mapping_.append(None) date_start, date_end = ( - datetime.strptime(fact[2].split('.')[0], '%Y-%m-%d %H:%M:%S'), - datetime.strptime(fact[1].split('.')[0], '%Y-%m-%d %H:%M:%S') + datetime.strptime(fact.start_time.split('.')[0], '%Y-%m-%d %H:%M:%S'), + datetime.strptime(fact.end_time.split('.')[0], '%Y-%m-%d %H:%M:%S') ) duration = ( date_start - date_end @@ -623,7 +572,7 @@ def _import(username, mapping_path=None, output=None, category_search=None, afte mapping_[0], mapping_[1], mapping_[2], - fact[3] or mapping_[4] or '', + fact.description or mapping_[4] or '', '0', # Exported mapping_[3], '', # Hourly rate @@ -633,21 +582,26 @@ def _import(username, mapping_path=None, output=None, category_search=None, afte output_file.close() -@kimai.command() -def dbinit(): +@kimai.group('db') +def db_(): + pass + + +@db_.command() +def init(): db.create_tables([KimaiCustomer, KimaiProject, KimaiActivity, HamsterKimaiMapping]) -@kimai.command() -def dbreset(): +@db_.command() +def reset(): HamsterKimaiMapping.delete().execute() -@kimai.command() +@db_.command() @click.option('-g', '--global', 'global_', help='Does this file contain mappings to global activties', is_flag=True) @click.option('--mapping-path', help='Mapping file') -def csv2db(mapping_path=None, global_=False): +def import_csv(mapping_path=None, global_=False): mapping_file = _get_kimai_mapping_file(mapping_path, None) next(mapping_file) mapping_reader = csv.reader(mapping_file) @@ -677,6 +631,7 @@ def csv2db(mapping_path=None, global_=False): kimai_tags=row[5] ) + @cli.command() def app(): from .app import HamsterToolsApp diff --git a/hamstertools/db.py b/hamstertools/db.py index a1e3452..0a59089 100644 --- a/hamstertools/db.py +++ b/hamstertools/db.py @@ -1,5 +1,5 @@ import logging -from peewee import SqliteDatabase, Model, CharField, ForeignKeyField +from peewee import SqliteDatabase, Model, CharField, ForeignKeyField, DateTimeField from textual.logging import TextualHandler @@ -29,6 +29,9 @@ class HamsterActivity(Model): class HamsterFact(Model): activity = ForeignKeyField(HamsterActivity, backref='facts') + start_time = DateTimeField() + end_time = DateTimeField(null=True) + description = CharField() class Meta: database = db