#!/usr/bin/env python3 """ Generate the Excel rulebook from rules.yaml (EV-008). USAGE python3 generate_rulebook_xlsx.py [output.xlsx] rules.yaml is the single source of truth. This script never edits it. The output must never be edited by hand (EV-007). """ import os import sys import yaml from openpyxl import Workbook from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from openpyxl.utils import get_column_letter HERE = os.path.dirname(os.path.abspath(__file__)) SRC = os.path.join(HERE, "rules.yaml") OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "PR_TBox_Rulebook.xlsx") FONT = "Arial" INK = "1F2933" HEAD_FILL = PatternFill("solid", fgColor="1F2933") BAND = { "Identifier": "E8F0F7", "Label": "EAF3EC", "Declaration": "FBF0E4", "Evolution": "F2EAF5", } MAJOR_FILL = PatternFill("solid", fgColor="FFF4CE") THIN = Side(style="thin", color="C9CFD6") BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) def selfcheck(doc): """EV-011 and EV-012 applied to the rulebook itself.""" problems = [] for r in doc["rules"]: ctl = r.get("control") or {} if not ctl.get("tier"): problems.append("%s: no control tier (EV-012)" % r["id"]) if r.get("severity") == "BLOCKING" and not ctl.get("executor"): problems.append("%s: BLOCKING without an executor (EV-011)" % r["id"]) if r.get("category") not in doc["categories"]: problems.append("%s: unknown category" % r["id"]) return problems def flow(t): return " ".join((t or "").split()) def joined(v): return ", ".join(v) if isinstance(v, list) else (v if v else "") def header(ws, cols): for i, (title, width) in enumerate(cols, start=1): c = ws.cell(row=1, column=i, value=title) c.font = Font(name=FONT, size=10, bold=True, color="FFFFFF") c.fill = HEAD_FILL c.alignment = Alignment(vertical="center", horizontal="left", wrap_text=True) c.border = BORDER ws.column_dimensions[get_column_letter(i)].width = width ws.row_dimensions[1].height = 28 ws.freeze_panes = ws.cell(row=2, column=1) ws.auto_filter.ref = "A1:%s1" % get_column_letter(len(cols)) def put(ws, r, c, value, wrap=False, bold=False, fill=None, size=10): cell = ws.cell(row=r, column=c, value=value) cell.font = Font(name=FONT, size=size, bold=bold, color=INK) cell.alignment = Alignment(vertical="top", wrap_text=wrap) cell.border = BORDER if fill: cell.fill = fill return cell def sheet_cover(wb, doc): m = doc["meta"] ws = wb.create_sheet("Cover") ws.column_dimensions["A"].width = 24 ws.column_dimensions["B"].width = 100 put(ws, 1, 1, m["title"], bold=True, size=14) rows = [ ("Version", "%s — %s" % (m["version"], m["status"])), ("Generated", "%s from rules.yaml" % m["date"]), ("Scope", flow(m["scope"])), ("Audience", flow(m["audience"])), ("Generation", "Generated from rules.yaml by generate_rulebook_xlsx.py. " "Do not edit this workbook: edit the source and regenerate " "(EV-007, EV-008)."), ] r = 3 for k, v in rows: put(ws, r, 1, k, bold=True) put(ws, r, 2, v, wrap=True) ws.row_dimensions[r].height = 15 * (len(v) // 95 + 1) r += 1 r += 1 put(ws, r, 1, "Categories", bold=True, size=12) r += 1 for k, v in doc["categories"].items(): put(ws, r, 1, k, bold=True, fill=PatternFill("solid", fgColor=BAND[k])) put(ws, r, 2, "%s — %s" % (v["title"], flow(v["intent"])), wrap=True) ws.row_dimensions[r].height = 30 r += 1 r += 1 put(ws, r, 1, "Counts", bold=True, size=12) r += 1 for label, formula in [ ("Rules", "=COUNTA(Rules!A2:A200)"), ("of which blocking", '=COUNTIF(Rules!D2:D200,"BLOCKING")'), ("Examples", "=COUNTA(Examples!A2:A400)"), ("Classes", "=COUNTA(Abstractness!A2:A200)"), ("of which abstract", "=COUNTIF(Abstractness!B2:B200,TRUE)"), ("Display entries", "=COUNTA(Display!A2:A200)"), ]: put(ws, r, 1, label) put(ws, r, 2, formula) r += 1 def sheet_rules(wb, doc): ws = wb.create_sheet("Rules") header(ws, [("Rule", 10), ("Category", 14), ("Title", 40), ("Severity", 11), ("Statement", 70), ("Applies to", 24), ("Enforced at", 14), ("Executor", 26), ("Procedure", 16), ("Related", 16), ("Why", 90)]) r = 2 for x in doc["rules"]: ctl = x.get("control") or {} band = PatternFill("solid", fgColor=BAND[x["category"]]) put(ws, r, 1, x["id"], bold=True, fill=band) put(ws, r, 2, x["category"], fill=band) put(ws, r, 3, x["title"], wrap=True, bold=True) put(ws, r, 4, x["severity"], fill=MAJOR_FILL if x["severity"] != "BLOCKING" else None) put(ws, r, 5, flow(x["statement"]), wrap=True) put(ws, r, 6, joined(x.get("scope")), wrap=True) put(ws, r, 7, joined(ctl.get("tier"))) put(ws, r, 8, ctl.get("executor") or "") put(ws, r, 9, ctl.get("procedure") or "pending") put(ws, r, 10, x.get("filiation") or "") put(ws, r, 11, flow(x.get("rationale")), wrap=True) ws.row_dimensions[r].height = 78 r += 1 def sheet_examples(wb, doc): ws = wb.create_sheet("Examples") header(ws, [("Rule", 10), ("Category", 14), ("From", 48), ("To", 48), ("Note", 62)]) r = 2 for x in doc["rules"]: for e in x.get("examples") or []: band = PatternFill("solid", fgColor=BAND[x["category"]]) put(ws, r, 1, x["id"], bold=True, fill=band) put(ws, r, 2, x["category"], fill=band) put(ws, r, 3, str(e["from"]), wrap=True) put(ws, r, 4, str(e["to"]), wrap=True) put(ws, r, 5, e.get("note") or "", wrap=True) r += 1 def sheet_abstractness(wb, doc): ws = wb.create_sheet("Abstractness") header(ws, [("Class", 36), ("isAbstract", 12), ("Note", 96)]) r = 2 for a in doc["abstractness"]: put(ws, r, 1, a["term"], bold=a["is_abstract"]) put(ws, r, 2, a["is_abstract"]) put(ws, r, 3, a.get("note") or "", wrap=True) r += 1 def sheet_display(wb, doc): ws = wb.create_sheet("Display") header(ws, [("IRI", 36), ("rdfs:label", 36), ("shortLabel", 22), ("acronym", 10), ("Note", 80)]) r = 2 for d in doc["display"]: put(ws, r, 1, d["iri"], bold=True) put(ws, r, 2, d["label"]) put(ws, r, 3, d.get("short_label") or "") put(ws, r, 4, d.get("acronym") or "") put(ws, r, 5, d.get("note") or "", wrap=True) r += 1 def main(): doc = yaml.safe_load(open(SRC, encoding="utf-8")) problems = selfcheck(doc) if problems: print("SELF-CHECK FAILED") for p in problems: print(" " + p) sys.exit(1) wb = Workbook() wb.remove(wb.active) sheet_cover(wb, doc) sheet_rules(wb, doc) sheet_examples(wb, doc) sheet_abstractness(wb, doc) sheet_display(wb, doc) wb.save(OUT) print("self-check passed: %d rules" % len(doc["rules"])) print("written: %s (%d sheets)" % (OUT, len(wb.sheetnames))) if __name__ == "__main__": main()