File size: 4,826 Bytes
63f41a6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | """Build the Leanstral-8B project workbook from the JSON sheets in ./data.
The xlsx is a BUILD ARTEFACT: edit data/*.json (or use the `journal` / `status`
subcommands below), never the workbook, or the next build will overwrite your edit.
Style follows the Duplex training workbook: one header row (bold white Calibri on
#305496, centred, wrapped), body cells top-aligned and wrapped, frozen header row,
autofilter over the used range, and Source / Confidence / Status columns.
python build_workbook.py # rebuild Leanstral_8B_Workbook.xlsx
python build_workbook.py journal "event" "result" "evidence" "next action"
python build_workbook.py status WBS 4.1 "Done" # set the Status cell of one row by Id
Docs tooling, not align runtime: it is outside the dependency firewall's scan and
may import openpyxl.
"""
from __future__ import annotations
import contextlib
import datetime as dt
import fcntl
import json
import os
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
DATA = HERE / "data"
OUT = HERE / "Leanstral_8B_Workbook.xlsx"
LOCK = DATA / ".workbook.lock"
@contextlib.contextmanager
def locked():
"""One writer at a time: journal/status are read-modify-write on data/*.json, and two concurrent
CLI calls would otherwise read the same snapshot and drop each other's change."""
LOCK.parent.mkdir(parents=True, exist_ok=True)
with open(LOCK, "w") as fh:
fcntl.flock(fh, fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(fh, fcntl.LOCK_UN)
# sheet order in the workbook; each name maps to data/<name>.json
SHEETS = ["README", "WBS", "Recipe_Stack", "Datasets", "Data_Gaps", "Evals_Gates",
"Experiment_Plan", "Journal", "Sources"]
def load(name: str) -> dict:
d = json.loads((DATA / f"{name}.json").read_text(encoding="utf-8"))
cols, widths = d["columns"], d["widths"]
if len(cols) != len(widths):
raise SystemExit(f"{name}: {len(cols)} columns but {len(widths)} widths")
for i, r in enumerate(d["rows"]):
if len(r) != len(cols):
raise SystemExit(f"{name} row {i + 2}: {len(r)} cells, expected {len(cols)}: {r[:2]}")
return d
def save(name: str, d: dict) -> None:
p = DATA / f"{name}.json"
tmp = p.with_name(f"{p.name}.tmp{os.getpid()}")
tmp.write_text(json.dumps(d, indent=1, ensure_ascii=False) + "\n", encoding="utf-8")
os.replace(tmp, p)
def build() -> Path:
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter
head_font = Font(name="Calibri", bold=True, color="FFFFFFFF")
head_fill = PatternFill("solid", fgColor="FF305496")
head_align = Alignment(horizontal="center", vertical="center", wrap_text=True)
body_font = Font(name="Calibri", size=11)
body_align = Alignment(vertical="top", wrap_text=True)
wb = Workbook()
wb.remove(wb.worksheets[0])
for name in SHEETS:
d = load(name)
ws = wb.create_sheet(name)
ws.append(d["columns"])
for c in ws[1]:
c.font, c.fill, c.alignment = head_font, head_fill, head_align
for r in d["rows"]:
ws.append(r)
for row in ws.iter_rows(min_row=2):
for c in row:
c.font, c.alignment = body_font, body_align
for i, w in enumerate(d["widths"], start=1):
ws.column_dimensions[get_column_letter(i)].width = w
ws.freeze_panes = "A2"
ws.auto_filter.ref = f"A1:{get_column_letter(len(d['columns']))}{ws.max_row}"
tmp = OUT.with_name(f"{OUT.stem}.tmp{os.getpid()}.xlsx")
wb.save(tmp)
os.replace(tmp, OUT)
return OUT
def journal(event: str, result: str, evidence: str, nxt: str) -> None:
d = load("Journal")
d["rows"].append([dt.date.today().isoformat(), event, result, evidence, nxt])
save("Journal", d)
def set_status(sheet: str, row_id: str, status: str) -> None:
d = load(sheet)
col = d["columns"].index("Status")
hits = [r for r in d["rows"] if r[0] == row_id]
if len(hits) != 1:
raise SystemExit(f"{sheet}: {len(hits)} rows with Id {row_id!r}")
hits[0][col] = status
save(sheet, d)
def main(argv: list[str]) -> int:
with locked():
return _main(argv)
def _main(argv: list[str]) -> int:
if argv[:1] == ["journal"]:
if len(argv) != 5:
raise SystemExit("usage: journal EVENT RESULT EVIDENCE NEXT")
journal(*argv[1:])
elif argv[:1] == ["status"]:
if len(argv) != 4:
raise SystemExit("usage: status SHEET ID STATUS")
set_status(*argv[1:])
elif argv:
raise SystemExit(__doc__)
print(build())
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
|