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]
|
||||
show_headers = False
|
||||
status_key = STATUS
|
||||
id_key = ID
|
||||
title_key = TITLE
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"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"
|
||||
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"
|
||||
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 json
|
||||
import datetime
|
||||
import textwrap
|
||||
from configparser import ConfigParser
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import click
|
||||
import tabulate
|
||||
|
|
@ -43,7 +45,11 @@ def load_data(context):
|
|||
|
||||
else:
|
||||
# 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:
|
||||
if context.obj['debug']:
|
||||
|
|
@ -61,6 +67,9 @@ def save_data(context, df):
|
|||
# Bring back ID as a regular column.
|
||||
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.
|
||||
with click.open_file(context.obj['input'], mode='w') as fd:
|
||||
df.to_csv(fd, index=False, quoting=csv.QUOTE_NONNUMERIC)
|
||||
|
|
@ -81,6 +90,17 @@ def configure(context, param, filename):
|
|||
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.
|
||||
@click.group()
|
||||
# Core options.
|
||||
|
|
@ -135,39 +155,111 @@ def cli(context, **kwargs):
|
|||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('TID', required=False, type=int, is_eager=True, callback=check_id)
|
||||
@click.pass_context
|
||||
def show(context):
|
||||
"""Show the kanban."""
|
||||
def show(context, tid):
|
||||
"""Show a task card (if ID is passed) or the whole the kanban (else)."""
|
||||
|
||||
df = load_data(context)
|
||||
if df.empty:
|
||||
print("No task.")
|
||||
return
|
||||
if tid is None:
|
||||
# Show the kanban tables.
|
||||
df = load_data(context)
|
||||
if df.empty:
|
||||
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)
|
||||
# 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 = ""
|
||||
for k in context.obj['show_keys']:
|
||||
if k not in df.columns:
|
||||
msg += "cannot show field `{}`, not found in `{}` ".format(k, context.obj['input'])
|
||||
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))
|
||||
# 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)
|
||||
# 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 = ""
|
||||
for k in context.obj['show_keys']:
|
||||
if k not in df.columns:
|
||||
msg += "cannot show field `{}`, not found in `{}` ".format(k, context.obj['input'])
|
||||
error("INVALID_KEY", msg)
|
||||
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()
|
||||
|
|
@ -192,22 +284,12 @@ def add(context, title, status, details, tags, deadline):
|
|||
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):
|
||||
"""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()
|
||||
@click.argument('TID', required=True, type=int, is_eager=True, callback=check_id)
|
||||
@click.option('-t', '--title' , type=str, prompt=True)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue