From ff2d313feaf56c5fbb8dcd3722de1ff83bea00b1 Mon Sep 17 00:00:00 2001 From: nojhan Date: Fri, 28 Jul 2023 11:36:15 +0200 Subject: [PATCH] fix indexing issues - add --show-headers general option. --- .klyban.csv | 15 +++--- klyban.py | 137 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 103 insertions(+), 49 deletions(-) diff --git a/.klyban.csv b/.klyban.csv index 7bc5aab..70580c7 100644 --- a/.klyban.csv +++ b/.klyban.csv @@ -1,6 +1,9 @@ -"STATUS","ID","TITLE","DETAILS","TAGS","DEADLINE" -"DOING",1.0,"print card","pretty print fixed-width cards given a content","klyban","" -"TODO",2.0,"print table","pretty print set of cards on each column","klyban","" -"TODO",3.0,"nested prints","print cards within cards","klyban","" -"TODO",4.0,"and yet another test","","","" -"TODO",5.0,"what","the fuck","","" +"ID","STATUS","TITLE","DETAILS","TAGS","DEADLINE" +1,"DOING","print card","pretty print fixed-width cards given a content","klyban","" +2,"TODO","print table","pretty print set of cards on each column","klyban","" +3,"TODO","nested prints","print cards within cards","klyban","" +4,"TODO","a test","","","" +5,"TODO","another test","","","" +6,"TODO","another test","","","" +7,"TODO","anothering test","","","" +8,"TODO","anothering test","","","" diff --git a/klyban.py b/klyban.py index 2e4ee61..74c2e18 100644 --- a/klyban.py +++ b/klyban.py @@ -15,9 +15,9 @@ error_codes = { } -def error(id,msg): +def error(name,msg): print("ERROR:",msg) - sys.exit(error_codes[id]) + sys.exit(error_codes[name]) def load_data(context): @@ -28,6 +28,7 @@ def load_data(context): # FIXME data sanity checks: unique index, aligned status, valid dates except FileNotFoundError: + # Create an empty file. df = pd.DataFrame(columns=[ context.obj['id_key'], context.obj['status_key'], @@ -39,12 +40,23 @@ def load_data(context): ]) save_data(context, df) - df.set_index(context.obj['id_key']) + # set index on TID. + df = df.astype({context.obj['id_key']:int}).set_index(context.obj['id_key']) + if context.obj['debug']: + print("Loaded:") + print(df) return df def save_data(context, df): + if context.obj['debug']: + print("Save:") + print(df) # FIXME double check that there are actually data. + + # Bring back TID as a regular column. + df = df.reset_index() + # Automagically manages standard input if input=="-", thanks to allow_dash=True. with click.open_file(context.obj['input'], mode='w') as fd: df.to_csv(fd, index=False, quoting=csv.QUOTE_NONNUMERIC) @@ -67,6 +79,7 @@ def configure(context, param, filename): # Global group holding global options. @click.group() +# Core options. @click.option( '-c', '--config', type = click.Path(dir_okay=False), @@ -78,6 +91,9 @@ def configure(context, param, filename): show_default = True, ) @click.option('-i', '--input' , help="CSV data file.", default='.klyban.csv', type=click.Path(writable=True, readable=True, allow_dash=True), show_default=True) +# Display options. +@click.option('-H','--show-headers', is_flag=True, help="Show the headers.") +# Low-level configuration options. @click.option('--status-key' , default='STATUS' , type=str, show_default=True, help="Header key defining the status of tasks.") @click.option('--status-list' , default='TODO,DOING,HOLD,DONE', type=str, show_default=True, help="Comma-separated, ordered list of possible values for the status of tasks.") @click.option('--id-key' , default='ID' , type=str, show_default=True, help="Header key defining the unique ID of tasks.") @@ -87,6 +103,7 @@ def configure(context, param, filename): @click.option('--deadline-key', default='DEADLINE', type=str, show_default=True, help="Header key defining the deadlines tasks.") @click.option('--touched-key', default='TOUCHED', type=str, show_default=True, help="Header key defining the deadlines tasks.") @click.option('--show-keys' , default='ID,TITLE,DETAILS,DEADLINE,TAGS', type=str , show_default=True, help="Comma-separated, ordered list of fields that should be shown") +@click.option('--debug', is_flag=True, help="Print debugging information.") @click.pass_context def cli(context, **kwargs): @@ -97,6 +114,8 @@ def cli(context, **kwargs): context.obj['input'] = kwargs['input'] + context.obj['show_headers'] = kwargs['show_headers'] + context.obj['id_key'] = kwargs['id_key'] context.obj['status_key'] = kwargs['status_key'] context.obj['title_key'] = kwargs['title_key'] @@ -108,6 +127,8 @@ def cli(context, **kwargs): context.obj['status_list'] = kwargs['status_list'].split(',') context.obj['show_keys'] = kwargs['show_keys'].split(',') + context.obj['debug'] = kwargs['debug'] + @cli.command() @click.pass_context @@ -119,13 +140,18 @@ def show(context): print("No task.") return + # Group by status. tables = df.groupby(context.obj['status_key']) + # Loop over the asked ordered status groups. for k in context.obj['status_list']: # Ordered. if k in tables.groups: df = tables.get_group(k) - # STATUS + # Bring back TID as a regular column. + df = df.reset_index() + # Print status as header. print(k) try: + # Print asked columns. t = df[context.obj['show_keys']] except KeyError as e: msg = "" @@ -134,80 +160,105 @@ def show(context): msg += "cannot show field `{}`, not found in `{}` ".format(k, context.obj['input']) error("INVALID_KEY", msg) else: - print(tabulate.tabulate(t.fillna(""), headers=context.obj['show_keys'], tablefmt="fancy_grid", showindex=False)) + if context.obj['show_headers']: + print(tabulate.tabulate(t.fillna(""), headers=context.obj['show_keys'], tablefmt="fancy_grid", showindex=False)) + else: + print(tabulate.tabulate(t.fillna(""), tablefmt="fancy_grid", showindex=False)) @cli.command() @click.argument('TITLE', required=True, nargs=-1) -@click.option('-d', '--details' , type=str, default="", prompt=True) -@click.option('-t', '--tags' , type=str, default="", prompt=True) -@click.option('-a', '--deadline', type=str, default="", prompt=True) +@click.option('-d', '--details' , type=str, default=None, prompt=True) +@click.option('-t', '--tags' , type=str, default=None, prompt=True) +@click.option('-a', '--deadline', type=str, default=None, prompt=True) @click.option('-s', '--status' , type=str, default='TODO') @click.pass_context def add(context, title, status, details, tags, deadline): """Add a new task.""" df = load_data(context) - next_id = df[context.obj['id_key']].max() + 1 - df.loc[next_id] = { - context.obj['id_key']:next_id, + next_id = df.index.max() + 1 + df.loc[next_id] = pd.Series({ context.obj['status_key']: status, context.obj['title_key']: " ".join(title), context.obj['details_key']: details, context.obj['tags_key']: tags, context.obj['deadline_key']: deadline, context.obj['touched_key']: datetime.datetime.now().isoformat(), - } + }) + # Remove any values consisting of empty spaces or quotes. + df = df.replace(r'^[\s"\']*$', float("nan"), regex=True) + save_data(context,df) context.invoke(show) def check_id(context, param, value): - """Eager callback, checking ID before edit's options prompting.""" + """Callback checking if task exists.""" + assert(type(value) == int) df = load_data(context) - if id not in df.index: + if value not in df.index: error("ID_NOT_FOUND", "{} `{}` was not found in data `{}`".format(context.obj['id_key'], value, context.obj['input'])) + return value @cli.command() -@click.argument('ID', required=True, type=int, is_eager=True, callback=check_id) +@click.argument('TID', required=True, type=int, is_eager=True, callback=check_id) @click.option('-t', '--title' , type=str, prompt=True) @click.option('-s', '--status' , type=str, prompt=True) @click.option('-d', '--details' , type=str, prompt=True, default="") @click.option('-t', '--tags' , type=str, prompt=True, default="") @click.option('-a', '--deadline', type=str, prompt=True, default="") -# FIXME populate the defaults with actual existing data. +# FIXME populate the defaults with the existing data. @click.pass_context -def edit(context, id, title, status, details, tags, deadline): +def edit(context, tid, title, status, details, tags, deadline): """Add a new task.""" df = load_data(context) - - df.loc[id] = { - context.obj['id_key']: id, - context.obj['status_key']: status, - context.obj['title_key']: " ".join(title), - context.obj['details_key']: details, - context.obj['tags_key']: tags, - context.obj['deadline_key']: deadline, - context.obj['touched_key']: datetime.datetime.now().isoformat(), - } + assert(tid in df.index) + df.loc[tid] = pd.Series({ + context.obj['status_key']: status, + context.obj['title_key']: title, + context.obj['details_key']: details, + context.obj['tags_key']: tags, + context.obj['deadline_key']: deadline, + context.obj['touched_key']: datetime.datetime.now().isoformat(), + }) save_data(context,df) context.invoke(show) +def check_yes(context, param, value): + """Callback cheking for explicit user consent.""" + if not value: + context.abort() + return value + @cli.command() -@click.argument('ID', required=True) +@click.argument('TID', required=True, type=int, is_eager=True, callback=check_id) +@click.option('-y', '--yes', is_flag=True, expose_value=False, callback=check_yes, prompt="Permanently delete task from records?") @click.pass_context -def promote(context, id): - """Upgrade the status of task `ID` to the next one. +def delete(context, tid): + """Delete a task.""" + df = load_data(context) + df = df.drop(index=tid) + save_data(context, df) + + context.invoke(show) + + +@cli.command() +@click.argument('TID', required=True, type=int) +@click.pass_context +def promote(context, tid): + """Upgrade the status of task `TID` to the next one. Use status names configured with --status-list.""" df = load_data(context) - row = df.loc[ df[context.obj['id_key']] == int(id) ] + row = df.loc[ df[context.obj['id_key']] == tid ] if row.empty: - error("ID_NOT_FOUND", "{} = {} not found in `{}`".format(context.obj['id_key'], id, context.obj['input'])) + error("ID_NOT_FOUND", "{} = {} not found in `{}`".format(context.obj['id_key'], tid, context.obj['input'])) i=0 for i in range(len(context.obj['status_list'])): @@ -216,10 +267,10 @@ def promote(context, id): else: i += 1 if i >= len(context.obj['status_list'])-1: - error("UNKNOWN_STATUS", "Cannot promote task {}, already at the last status.".format(id)) + error("UNKNOWN_STATUS", "Cannot promote task {}, already at the last status.".format(tid)) else: - df.loc[df[context.obj['id_key']] == int(id), context.obj['status_key']] = context.obj['status_list'][i+1] - df.loc[df[context.obj['id_key']] == int(id), context.obj['touched_key']] = datetime.datetime.now().isoformat() + df.loc[df[context.obj['id_key']] == tid, context.obj['status_key']] = context.obj['status_list'][i+1] + df.loc[df[context.obj['id_key']] == tid, context.obj['touched_key']] = datetime.datetime.now().isoformat() save_data(context, df) @@ -227,18 +278,18 @@ def promote(context, id): @cli.command() -@click.argument('ID', required=True) +@click.argument('TID', required=True, type=int) @click.pass_context -def demote(context, id): - """Downgrade the status of task `ID` to the previous one. +def demote(context, tid): + """Downgrade the status of task `TID` to the previous one. Use status names configured with --status-list.""" df = load_data(context) - row = df.loc[ df[context.obj['id_key']] == int(id) ] + row = df.loc[ df[context.obj['id_key']] == tid ] if row.empty: - error("ID_NOT_FOUND", "{} = {} not found in `{}`".format(context.obj['id_key'], id, context.obj['input'])) + error("ID_NOT_FOUND", "{} = {} not found in `{}`".format(context.obj['id_key'], tid, context.obj['input'])) i=0 for i in range(len(context.obj['status_list'])): @@ -247,10 +298,10 @@ def demote(context, id): else: i += 1 if i == 0: - error("UNKNOWN_STATUS", "Cannot demote task {}, already at the first status.".format(id)) + error("UNKNOWN_STATUS", "Cannot demote task {}, already at the first status.".format(tid)) else: - df.loc[df[context.obj['id_key']] == int(id), context.obj['status_key']] = context.obj['status_list'][i-1] - df.loc[df[context.obj['id_key']] == int(id), context.obj['touched_key']] = datetime.datetime.now().isoformat() + df.loc[df[context.obj['id_key']] == tid, context.obj['status_key']] = context.obj['status_list'][i-1] + df.loc[df[context.obj['id_key']] == tid, context.obj['touched_key']] = datetime.datetime.now().isoformat() save_data(context, df)