219 lines
7.2 KiB
Python
219 lines
7.2 KiB
Python
|
|
#!/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()
|