Model/infrastructure/xlsx/scenario_export_workbook.py
Khalim Conn-Kowlessar e313f19ef2 Brand the export header band and freeze the header row 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 09:22:44 +00:00

58 lines
2 KiB
Python

"""Renders the Scenario Export workbook (ADR-0065).
One ``.xlsx`` with one sheet per Scenario (model A2), each sheet's rows shaped by
``domain.scenario_export`` and painted with the Monday-style branding in
``infrastructure.xlsx.branding``. Programmatic openpyxl (no template) so a
dynamic number of scenario sheets is handled natively.
"""
from __future__ import annotations
import re
from io import BytesIO
from typing import Sequence
from openpyxl import Workbook
from domain.scenario_export.scenario_sheet import ExportSheet
from infrastructure.xlsx.branding import style_sheet
# Excel forbids these in a sheet title and caps titles at 31 characters.
_ILLEGAL_SHEET_CHARS = re.compile(r"[:\\/?*\[\]]")
_MAX_SHEET_NAME = 31
def _safe_sheet_name(name: str, used: set[str]) -> str:
"""An Excel-legal, unique sheet name: illegal characters stripped, capped at
31 chars, and suffixed on collision (two scenarios can share a name)."""
clean = _ILLEGAL_SHEET_CHARS.sub("", name or "scenario").strip() or "scenario"
clean = clean[:_MAX_SHEET_NAME]
base, index = clean, 1
while clean in used:
suffix = f" ({index})"
clean = base[: _MAX_SHEET_NAME - len(suffix)] + suffix
index += 1
used.add(clean)
return clean
def render_workbook(sheets: Sequence[tuple[str, ExportSheet]]) -> bytes:
"""Render the scenario sheets into a single branded ``.xlsx`` as bytes. Each
tuple is a Scenario name and its shaped sheet; sheet order follows the input
(the requested ``scenario_ids`` order)."""
workbook = Workbook()
default = workbook.active
if default is not None:
workbook.remove(default)
used_names: set[str] = set()
for name, sheet in sheets:
worksheet = workbook.create_sheet(title=_safe_sheet_name(name, used_names))
worksheet.append(list(sheet.columns))
for row in sheet.rows:
worksheet.append([row.get(column, "") for column in sheet.columns])
style_sheet(worksheet)
buffer = BytesIO()
workbook.save(buffer)
return buffer.getvalue()