feat: add show ID

This commit is contained in:
Johann Dreo 2023-07-28 17:11:23 +02:00
commit 00c4731d75
3 changed files with 126 additions and 43 deletions

View file

@ -1,5 +1,6 @@
[options] [options]
show_headers = False
status_key = STATUS status_key = STATUS
id_key = ID id_key = ID
title_key = TITLE title_key = TITLE

View file

@ -1,5 +1,5 @@
"ID","STATUS","TITLE","DETAILS","TAGS","DEADLINE","TOUCHED" "ID","STATUS","TITLE","DETAILS","TAGS","DEADLINE","TOUCHED"
0,"TODO","Use click-option-group","To help sort options in categories in help.","","","2023-07-28T12:04:02.615501" 0,"TODO","Use click-option-group","To help sort options in categories in help.","","","2023-07-28T12:04:02.615501"
1,"TODO","Use click-aliases","To allow for aliases (TBC: user-defined in config file?)","","","2023-07-28T12:05:04.229519" 1,"TODO","Use click-aliases","To allow for aliases (TBC: user-defined in config file?)","UX","","2023-07-28T17:10:35.635275"
2,"TODO","edit existing","When calling edit, populate defaults with existing data.","","","2023-07-28T12:07:08.177802" 2,"TODO","edit existing","When calling edit, populate defaults with existing data.","","","2023-07-28T12:07:08.177802"
3,"TODO","sanity checks","Check data consistency in load_data and save_data.","","","2023-07-28T12:08:10.272349" 3,"TODO","sanity checks","Check data consistency in load_data and save_data.","","","2023-07-28T12:08:10.272349"

1 ID STATUS TITLE DETAILS TAGS DEADLINE TOUCHED
2 0 TODO Use click-option-group To help sort options in categories in help. 2023-07-28T12:04:02.615501
3 1 TODO Use click-aliases To allow for aliases (TBC: user-defined in config file?) UX 2023-07-28T12:05:04.229519 2023-07-28T17:10:35.635275
4 2 TODO edit existing When calling edit, populate defaults with existing data. 2023-07-28T12:07:08.177802
5 3 TODO sanity checks Check data consistency in load_data and save_data. 2023-07-28T12:08:10.272349

162
klyban.py
View file

@ -2,8 +2,10 @@ import sys
import csv import csv
import json import json
import datetime import datetime
import textwrap
from configparser import ConfigParser from configparser import ConfigParser
import numpy as np
import pandas as pd import pandas as pd
import click import click
import tabulate import tabulate
@ -43,7 +45,11 @@ def load_data(context):
else: else:
# set index on ID. # set index on ID.
df = df.astype({context.obj['id_key']:int}).set_index(context.obj['id_key']) df = df.astype({context.obj['id_key']:int})
df = df.set_index(context.obj['id_key'])
# Remove any values consisting of empty spaces or quotes.
df = df.replace(r'^[\s"\']*$', np.nan, regex=True)
finally: finally:
if context.obj['debug']: if context.obj['debug']:
@ -61,6 +67,9 @@ def save_data(context, df):
# Bring back ID as a regular column. # Bring back ID as a regular column.
df = df.reset_index() df = df.reset_index()
# Remove any values consisting of empty spaces or quotes.
df = df.replace(r'^[\s"\']*$', np.nan, regex=True)
# Automagically manages standard input if input=="-", thanks to allow_dash=True. # Automagically manages standard input if input=="-", thanks to allow_dash=True.
with click.open_file(context.obj['input'], mode='w') as fd: with click.open_file(context.obj['input'], mode='w') as fd:
df.to_csv(fd, index=False, quoting=csv.QUOTE_NONNUMERIC) df.to_csv(fd, index=False, quoting=csv.QUOTE_NONNUMERIC)
@ -81,6 +90,17 @@ def configure(context, param, filename):
defaults.update(cfg[sect]) defaults.update(cfg[sect])
def check_id(context, param, value):
"""Callback checking if task exists."""
if value is None: # For optional TID.
return value
assert(type(value) == int)
df = load_data(context)
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
# Global group holding global options. # Global group holding global options.
@click.group() @click.group()
# Core options. # Core options.
@ -135,39 +155,111 @@ def cli(context, **kwargs):
@cli.command() @cli.command()
@click.argument('TID', required=False, type=int, is_eager=True, callback=check_id)
@click.pass_context @click.pass_context
def show(context): def show(context, tid):
"""Show the kanban.""" """Show a task card (if ID is passed) or the whole the kanban (else)."""
df = load_data(context) if tid is None:
if df.empty: # Show the kanban tables.
print("No task.") df = load_data(context)
return if df.empty:
print("No task.")
return
# Group by status. # Group by status.
tables = df.groupby(context.obj['status_key']) tables = df.groupby(context.obj['status_key'])
# Loop over the asked ordered status groups. # Loop over the asked ordered status groups.
for k in context.obj['status_list']: # Ordered. for k in context.obj['status_list']: # Ordered.
if k in tables.groups: if k in tables.groups:
df = tables.get_group(k) df = tables.get_group(k)
# Bring back TID as a regular column. # Bring back TID as a regular column.
df = df.reset_index() df = df.reset_index()
# Print status as header. # Print status as header.
print(k) print(k)
try: try:
# Print asked columns. # Print asked columns.
t = df[context.obj['show_keys']] t = df[context.obj['show_keys']]
except KeyError as e: except KeyError as e:
msg = "" msg = ""
for k in context.obj['show_keys']: for k in context.obj['show_keys']:
if k not in df.columns: if k not in df.columns:
msg += "cannot show field `{}`, not found in `{}` ".format(k, context.obj['input']) msg += "cannot show field `{}`, not found in `{}` ".format(k, context.obj['input'])
error("INVALID_KEY", msg) error("INVALID_KEY", msg)
else:
if context.obj['show_headers']:
print(tabulate.tabulate(t.fillna(""), headers=context.obj['show_keys'], tablefmt="fancy_grid", showindex=False))
else: else:
print(tabulate.tabulate(t.fillna(""), 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))
else: # tid is not None.
# Show a task card.
df = load_data(context)
row = df.loc[tid]
t_label = ["", "", ""]
t_top = ["", "", "", "", ""]
t_body = ["", " ", ""]
t_sep = ["", "", ""]
t_bottom = ["", "", ""]
width = 30
# Label content.
l = []
if context.obj['id_key'] in context.obj['show_keys']:
l.append(str(tid))
if context.obj['title_key'] in context.obj['show_keys']:
l.append(row[context.obj['title_key']])
lbl = ":".join(l)
label = textwrap.shorten(lbl, width=width, placeholder="")
# Label format.
card = t_label[0] + t_label[1]*len(label) + t_label[2] + "\n"
card += t_body[0] + label + t_body[2] + "\n"
card += t_top[0] + t_top[1]*len(label) + t_top[2] + t_top[3]*(width-len(label)-1) + t_top[4] + "\n"
if context.obj['details_key'] in context.obj['show_keys']:
if str(row[context.obj['details_key']]) != "nan": # FIXME WTF?
d = row[context.obj['details_key']]
else:
d = ''
details = textwrap.wrap(d, width)
for line in details:
card += t_body[0] + line + t_body[1]*(width-len(line)) + t_body[2] + "\n"
if context.obj['tags_key'] in context.obj['show_keys']:
card += t_sep[0] + t_sep[1]*width + t_sep[2] + "\n"
if str(row[context.obj['tags_key']]) != "nan": # FIXME WTF?
t = row[context.obj['tags_key']]
else:
t = ''
tags = textwrap.wrap(t, width)
for line in tags:
card += t_body[0] + line + t_body[1]*(width-len(line)) + t_body[2] + "\n"
if context.obj['deadline_key'] in context.obj['show_keys']:
card += t_sep[0] + t_sep[1]*width + t_sep[2] + "\n"
if str(row[context.obj['deadline_key']]) != "nan": # FIXME WTF?
t = row[context.obj['deadline_key']]
else:
t = ''
deadline = textwrap.wrap(t, width)
for line in deadline:
card += t_body[0] + line + t_body[1]*(width-len(line)) + t_body[2] + "\n"
if context.obj['touched_key'] in context.obj['show_keys']:
card += t_sep[0] + t_sep[1]*width + t_sep[2] + "\n"
if str(row[context.obj['touched_key']]) != "nan": # FIXME WTF?
t = row[context.obj['touched_key']]
else:
t = ''
touched = textwrap.wrap(t, width)
for line in touched:
card += t_body[0] + line + t_body[1]*(width-len(line)) + t_body[2] + "\n"
card += t_bottom[0] + t_bottom[1]*width + t_bottom[2] # No newline.
print(card)
@cli.command() @cli.command()
@ -192,22 +284,12 @@ def add(context, title, status, details, tags, deadline):
context.obj['deadline_key']: deadline, context.obj['deadline_key']: deadline,
context.obj['touched_key']: datetime.datetime.now().isoformat(), 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) save_data(context,df)
context.invoke(show) context.invoke(show)
def check_id(context, param, value):
"""Callback checking if task exists."""
assert(type(value) == int)
df = load_data(context)
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() @cli.command()
@click.argument('TID', 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('-t', '--title' , type=str, prompt=True)