fix indexing issues

- add --show-headers general option.
This commit is contained in:
Johann Dreo 2023-07-28 11:36:15 +02:00
commit ff2d313fea
2 changed files with 103 additions and 49 deletions

View file

@ -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","","",""

1 ID STATUS TITLE DETAILS TAGS DEADLINE
2 1.0 1 DOING print card pretty print fixed-width cards given a content klyban
3 2.0 2 TODO print table pretty print set of cards on each column klyban
4 3.0 3 TODO nested prints print cards within cards klyban
5 4.0 4 TODO and yet another test a test
6 5.0 5 TODO what another test the fuck
7 6 TODO another test
8 7 TODO anothering test
9 8 TODO anothering test

137
klyban.py
View file

@ -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)