"""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 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]], destination_path: str ) -> None: """Render the scenario sheets into a single branded ``.xlsx`` at ``destination_path``. Each tuple is a Scenario name and its shaped sheet; sheet order follows the input (the requested ``scenario_ids`` order). Saves straight to disk (``Workbook.save(path)``) so the caller can ``S3Client.upload_file`` it with multipart — the workbook is never also held as an in-memory ``bytes`` copy (ADR-0065; the OOM path the 4 GB sizing was against at the 100k-row cap). The further memory win is openpyxl ``write_only`` mode; that switch would live at this seam. """ 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) workbook.save(destination_path)