fix indexing issues
- add --show-headers general option.
This commit is contained in:
parent
b26e384000
commit
ff2d313fea
2 changed files with 103 additions and 49 deletions
15
.klyban.csv
15
.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","","",""
|
||||
|
|
|
|||
|
137
klyban.py
137
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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue