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 = "" |