From 62451aa3c9b888ef9c536d20d08c60fad1c9ef2e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 14 Jul 2026 09:18:59 +0000 Subject: [PATCH] =?UTF-8?q?Render=20the=20scenario=20export=20workbook=20w?= =?UTF-8?q?ith=20a=20sheet=20per=20scenario=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- infrastructure/xlsx/__init__.py | 0 .../xlsx/scenario_export_workbook.py | 20 +++++++++++++ tests/infrastructure/xlsx/__init__.py | 0 .../xlsx/test_scenario_export_workbook.py | 30 +++++++++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 infrastructure/xlsx/__init__.py create mode 100644 infrastructure/xlsx/scenario_export_workbook.py create mode 100644 tests/infrastructure/xlsx/__init__.py create mode 100644 tests/infrastructure/xlsx/test_scenario_export_workbook.py diff --git a/infrastructure/xlsx/__init__.py b/infrastructure/xlsx/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/infrastructure/xlsx/scenario_export_workbook.py b/infrastructure/xlsx/scenario_export_workbook.py new file mode 100644 index 000000000..0da8a6f4f --- /dev/null +++ b/infrastructure/xlsx/scenario_export_workbook.py @@ -0,0 +1,20 @@ +"""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 + +from typing import Sequence + +from domain.scenario_export.scenario_sheet import ExportSheet + + +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).""" + raise NotImplementedError diff --git a/tests/infrastructure/xlsx/__init__.py b/tests/infrastructure/xlsx/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/infrastructure/xlsx/test_scenario_export_workbook.py b/tests/infrastructure/xlsx/test_scenario_export_workbook.py new file mode 100644 index 000000000..d1dfaa79c --- /dev/null +++ b/tests/infrastructure/xlsx/test_scenario_export_workbook.py @@ -0,0 +1,30 @@ +"""The Scenario Export workbook renderer (ADR-0065): one branded .xlsx with one +sheet per Scenario, headers then rows, verified by re-opening the bytes.""" + +from __future__ import annotations + +from io import BytesIO + +from openpyxl import load_workbook + +from domain.scenario_export.scenario_sheet import ExportSheet +from infrastructure.xlsx.scenario_export_workbook import render_workbook + + +def test_renders_a_sheet_named_for_the_scenario_with_headers_then_rows() -> None: + # arrange — one scenario's shaped sheet. + sheet = ExportSheet( + columns=("property_id", "loft_insulation"), + rows=({"property_id": 1, "loft_insulation": 1200.0},), + ) + + # act + data = render_workbook([("Fabric first", sheet)]) + + # assert — a real workbook with a sheet named for the scenario, the column + # contract as the header row, and the data beneath it. + workbook = load_workbook(BytesIO(data)) + assert workbook.sheetnames == ["Fabric first"] + worksheet = workbook["Fabric first"] + assert [cell.value for cell in worksheet[1]] == ["property_id", "loft_insulation"] + assert [cell.value for cell in worksheet[2]] == [1, 1200.0]