feat: add show ID
This commit is contained in:
parent
68915c9d2f
commit
00c4731d75
3 changed files with 126 additions and 43 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
162
klyban.py
162
klyban.py
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue