diff options
Diffstat (limited to 'scripts')
-rwxr-xr-x | scripts/reqs2tex.py | 132 | ||||
-rw-r--r-- | scripts/tex.py | 37 | ||||
-rwxr-xr-x | scripts/time2tex.py | 111 |
3 files changed, 184 insertions, 96 deletions
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py index 6b7b77a..e5f063d 100755 --- a/scripts/reqs2tex.py +++ b/scripts/reqs2tex.py @@ -1,17 +1,48 @@ #!/bin/python3 import sys, tomllib, tex +from enum import StrEnum + +def label2ref(*labels): + return ",".join(["req:" + label.replace('.', ':') for label in labels]) + +class KEY(StrEnum): + LABEL = 'label' + TYPE = 'type' + ID = 'id' + INDEX = 'index' + DELETED = 'deleted' + DONE = 'done' + DESCRIPTION = 'description' + PRIORITY = 'priority' + +class REQ_TYPE(StrEnum): + SYSTEM = 'system' + USER = 'user' + +class REQ_PRIORITY(StrEnum): + MUST = 'must' + SHOULD = 'should' + COULD = 'could' + WONT = 'will not' def flatten(data): - if 'description' in data: - return [ data ] out = [] for key, value in data.items(): + # this item is a requirement + if key == KEY.DESCRIPTION: + out.append(data) + + # skip over reserved keys + if key in KEY: continue + + # recursively flatten other requirements items = flatten(value) + # and prefix them with the current key for item in items: - if 'label' in item: - item['label'] = f"{key}.{item['label']}" + if KEY.LABEL in item: + item[KEY.LABEL] = f"{key}.{item[KEY.LABEL]}" else: - item['label'] = f"{key}" + item[KEY.LABEL] = f"{key}" out += items return out @@ -20,72 +51,99 @@ def make_id(item): global id_counter id_counter += 1 return "{type_short}#{counter:03d}".format( - type_short = item['type'][0].upper(), + type_short = item[KEY.TYPE][0].upper(), counter = id_counter, ) def sanitize(item, ids): def die(msg): - print(f"[{item['label']}]: {msg}") + print(f"[{item[KEY.LABEL]}]: {msg}") exit(1) # ensure properties - item['description'] = item.get('description') - item['done'] = item.get('done') - item['priority'] = item.get('priority') - item['type'] = item.get('type') + item[KEY.DESCRIPTION] = item.get(KEY.DESCRIPTION) + item[KEY.DONE] = item.get(KEY.DONE) + item[KEY.PRIORITY] = item.get(KEY.PRIORITY) + item[KEY.TYPE] = item.get(KEY.TYPE) # type checks - if item['type'] not in ['system', 'user']: - die(f"unknown or missing requirement type {repr(item['type'])}") - if item['priority'] not in ['must', 'should', 'could', 'will not']: - die(f"unknown or missing requirement priority {repr(item['type'])}") + if item[KEY.TYPE] not in REQ_TYPE: + die(f"unknown or missing requirement type: {repr(item[KEY.TYPE])}") + if item[KEY.PRIORITY] not in REQ_PRIORITY: + die(f"unknown or missing requirement priority: {repr(item[KEY.PRIORITY])}") # conversions - if isinstance(item['done'], list): + if isinstance(item[KEY.DONE], list): # safety check - if not set(item['done']).issubset(ids): + if not set(item[KEY.DONE]).issubset(ids): die("definition of done includes unknown requirement(s)") - item['done'] = tex.cmd('Cref', tex.label2ref(*item['done'])) + item[KEY.DONE] = tex.cmd('Cref', label2ref(*item[KEY.DONE])) def convert(data): reqs = flatten(data) + all_ids = [item[KEY.LABEL] for item in reqs] index = 0 for item in reqs: - item['id'] = tex.esc(make_id(item)) - item['deleted'] = item.get('deleted', False) - if item['deleted']: continue - item['index'] = index + item[KEY.ID] = tex.esc(make_id(item)) + item[KEY.DELETED] = item.get(KEY.DELETED, False) + if item[KEY.DELETED]: continue + item[KEY.INDEX] = index index += 1 - sanitize(item, [req['label'] for req in reqs]) + sanitize(item, all_ids) # skip deleted requirements (but process for make_id) - reqs = [item for item in reqs if item['deleted'] == False] + reqs = [item for item in reqs if item[KEY.DELETED] == False] return reqs def fmt_aux(data): out = [] - for req in data: - ref = tex.label2ref(req['label']) + for item in data: + ref = label2ref(item[KEY.LABEL]) out += [ - tex.cmd('newlabel', f"{ref}", tex.group(req['id'], req['id'], 'ggg', 'hhh', 'iii')), - tex.cmd('newlabel', f"{ref}@cref", tex.group(f"[requirement][aaa][bbb]{req['id']}", '[ccc][ddd][eee]fff')), + tex.cmd('newlabel', f"{ref}", tex.group( + item[KEY.ID], + '', + '', + ref, + '', + )), + tex.cmd('newlabel', f"{ref}@cref", tex.group( + f"[requirement][][]{item[KEY.ID]}", + '[][][]', + '', + '', + '', + )), ] return "\n".join(out) def fmt_tex(data): - out = [] - for req in data: - out.append( - tex.cmd('subsection', req['id']) + "\n\n" +\ - tex.env('description', - tex.cmd('item', ['Priority']) + req['priority'].title() +\ - tex.cmd('item', ['Requirement']) + req['description'] +\ - (tex.cmd('item', ['Definition of done']) + req['done'] if req['done'] is not None else "") + out = "" + for item in data: + out += tex.join( + tex.cmd('subsection', f"{item[KEY.ID]}: {item[KEY.LABEL]}".upper()), + tex.withatletter( + tex.cmd('cref@constructprefix', 'requirement', r'\cref@result'), + tex.pedef('@currentlabel', item[KEY.ID]), + tex.pedef('@currentlabelname', item[KEY.ID]), + tex.pedef('cref@currentlabel', tex.group(['requirement'], [''], [r'\cref@result']) + item[KEY.ID]), + ), + tex.cmd('label', ['requirement'], label2ref(item[KEY.LABEL])), + tex.cmd('parbox', tex.cmd('linewidth'), + tex.env('description', tex.join( + tex.cmd('item', [tex.cmd('reqlabel', 'priority')]), + item[KEY.PRIORITY].title(), + tex.cmd('item', [tex.cmd('reqlabel', 'description')]), + item[KEY.DESCRIPTION], + *([ + tex.cmd('item', [tex.cmd('reqlabel', 'done')]), + item[KEY.DONE] + ] if item[KEY.DONE] is not None else []), + )), ) ) - return "\n\n".join(out) + return out def main(input_file): data = {} diff --git a/scripts/tex.py b/scripts/tex.py index 2509a87..07d275a 100644 --- a/scripts/tex.py +++ b/scripts/tex.py @@ -9,6 +9,9 @@ def group(*args): out += "{" + arg + "}" return out +def join(*things): + return "".join(things) + def string(content): return r"\string" + content @@ -18,11 +21,14 @@ def cmd(*args): if len(args) == 0: args = [""] return f"\\{name}" + group(*args) +def pedef(*args): + return r"\protected@edef" + cmd(*args) + def csdef(*args): return r"\def" + cmd(*args) -def auxout(content): - return r"\write\@auxout" + group(content) +def auxout(*content): + return r"\write\@auxout" + group(join(*content)) def scmd(*args): return string(cmd(*args)) @@ -47,6 +53,29 @@ def esc(plain): def tabrule(*cells): return "&".join(cells) + "\\\\" -def label2ref(*labels): - return ",".join(["req:" + label.replace('.', ':') for label in labels]) +def withatletter(*content): + return join( + cmd('makeatletter'), + *content, + cmd('makeatother'), + ) + +def explist(*items): + out = [] + for item in items: + if isinstance(item, str) or not hasattr(item, '__iter__'): + out.append(item) + else: + out += explist(*item) + return out + +def sec(level, heading): + level = max(min(3, level), 0) + section = [ + 'section', + 'subsection', + 'subsubsection', + 'paragraph', + ][level] + return cmd(section, heading) diff --git a/scripts/time2tex.py b/scripts/time2tex.py index 8c3dd9b..a5d6802 100755 --- a/scripts/time2tex.py +++ b/scripts/time2tex.py @@ -35,35 +35,30 @@ def fmt_member_overview(times): tracked[time["name"]] += time["duration"] total_time += time["duration"] - out = "" - - # header - out += tex.cmd('toprule') - out += tex.tabrule(tex.cmd('textbf', 'Member'), tex.cmd('textbf', 'Tracked'), '') - out += tex.cmd('midrule') - - # member overview members = sorted(list(set(time["name"] for time in times))) - for name in members: - out += tex.tabrule(name, fmt_duration(tracked[name]), fmt_percentage(tracked[name] / total_time)) - out += tex.cmd('midrule') - - # sum - out += tex.tabrule('', fmt_duration(total_time), '') - out += tex.cmd('bottomrule') - - out = tex.env('tabular', 'lr@{~}l', out) - out = tex.cmd('centering') +\ - out +\ - tex.cmd('caption', 'Tracked time per group member') +\ - tex.cmd('label', 'tab:time-member') - out = tex.env('table', out) - - return out + return tex.env('table', tex.join( + tex.cmd('centering'), + tex.env('tabular', 'lr@{~}l', tex.join( + tex.cmd('toprule'), + tex.tabrule(tex.cmd('textbf', 'Member'), tex.cmd('textbf', 'Tracked')), + tex.cmd('midrule'), + *[ + tex.tabrule( + name, + fmt_duration(tracked[name]), + fmt_percentage(tracked[name] / total_time)) + for name in members + ], + tex.cmd('midrule'), + tex.tabrule('', fmt_duration(total_time), ''), + tex.cmd('bottomrule'), + )), + tex.cmd('caption', 'Tracked time per group member'), + tex.cmd('label', 'tab:time-member'), + )) def fmt_weekly_overview(times): # calculations - out = "" weeks = [] member_totals = {} total_time = sum(time["duration"] for time in times) @@ -93,34 +88,40 @@ def fmt_weekly_overview(times): for member in members: member_totals[member] = sum(time["duration"] for time in times if time["name"] == member) - # TODO: refactor - # begin table - out += r"\begin{table}\centering" - out += r"\fitimg{" - out += f"\\begin{{tabular}}{{l{'r@{~}l' * len(members)}@{{\\qquad}}r}}\\toprule" - out += r"\textbf{\#}" - for member in members: - out += f"&\\textbf{{{member}}}&" - out += r"&\textbf{Subtotal}\\\midrule{}" - - for entry in weeks: - out += f"{entry['num']}" - for member in members: - out += f"&{fmt_duration(entry['members'][member])}&{fmt_percentage(entry['members'][member] / entry['total'])}" - out += f"&{fmt_duration(entry['total'])}\\\\" - - out += r"\midrule{}" - for member in members: - out += f"&{fmt_duration(member_totals[member])}&{fmt_percentage(member_totals[member] / total_time)}" - out += f"&{fmt_duration(total_time)}\\\\" - - # end table - out += r"\bottomrule\end{tabular}" - out += r"}" # \fitimg - out += r"\caption{Tracked time per week}\label{tab:time-weekly}" - out += r"\end{table}" - - return out + return tex.env('table', tex.join( + tex.cmd('centering'), + tex.cmd('fitimg', + tex.env('tabular', r'l' + r'r@{~}l' * len(members) + r'@{\qquad}r', tex.join( + tex.cmd('toprule'), + tex.tabrule(*[ + tex.cmd('textbf', cell) + for cell in [ + tex.esc("#"), + *tex.explist([ member, "" ] for member in members), + "Subtotal", + ] + ]), + tex.cmd('midrule'), + *[ + tex.tabrule(*[ + str(entry['num']), + *tex.explist( + [ + fmt_duration(entry['members'][member]), + fmt_percentage(entry['members'][member] / entry['total']), + ] + for member in members + ), + fmt_duration(entry['total']), + ]) + for entry in weeks + ], + tex.cmd('bottomrule'), + )), + ), + tex.cmd('caption', 'Tracked time per week'), + tex.cmd('label', 'tab:time-weekly'), + )) def duration2secs(duration): out = 0 # output (seconds) @@ -182,13 +183,13 @@ def parse(content): return out def fmt(times): - return "\n\n".join([ + return tex.join( tex.cmd('section', 'Overviews'), tex.cmd('subsection', 'Members'), fmt_member_overview(times), tex.cmd('subsection', 'Weekly'), fmt_weekly_overview(times), - ]) + ) def main(input_file): content = "" |