mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Dan's review on #1546: - Stream the workbook to a /tmp temp file and S3Client.upload_file it, instead of render_workbook -> bytes -> put_object. render_workbook now saves straight to a path; the orchestrator renders to a temp file, multipart-uploads it, and always cleans it up. Restores ADR-0065's "never an in-memory BytesIO" decision (the OOM path at the 100k-row cap). - Move the raw `SELECT ... FROM scenario` out of the Lambda handler into ScenarioNamesPostgresRepository, so the handler stays composition-only and all SQL lives in repositories/. - current_sap_points goes through _float, matching its Optional[float] read-model type and the sibling numeric facts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
75 lines
2.7 KiB
Python
75 lines
2.7 KiB
Python
"""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 pathlib import Path
|
|
|
|
from openpyxl import load_workbook
|
|
|
|
from domain.scenario_export.scenario_sheet import ExportSheet
|
|
from infrastructure.xlsx.branding import HEADER_FILL
|
|
from infrastructure.xlsx.scenario_export_workbook import render_workbook
|
|
|
|
|
|
def test_renders_a_sheet_named_for_the_scenario_with_headers_then_rows(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
# arrange — one scenario's shaped sheet.
|
|
sheet = ExportSheet(
|
|
columns=("property_id", "loft_insulation"),
|
|
rows=({"property_id": 1, "loft_insulation": 1200.0},),
|
|
)
|
|
|
|
# act
|
|
path = str(tmp_path / "export.xlsx")
|
|
render_workbook([("Fabric first", sheet)], path)
|
|
|
|
# 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(path)
|
|
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]
|
|
|
|
|
|
def test_sanitises_and_deduplicates_scenario_sheet_names(tmp_path: Path) -> None:
|
|
# arrange — a name over Excel's 31-char limit (used twice, so it must be
|
|
# deduplicated) and one with characters Excel forbids in a sheet title.
|
|
empty = ExportSheet(columns=("property_id",), rows=())
|
|
long_name = "A really long scenario name that exceeds Excel's limit"
|
|
illegal_name = "Bad:/\\?*[]name"
|
|
|
|
# act
|
|
path = str(tmp_path / "export.xlsx")
|
|
render_workbook(
|
|
[(long_name, empty), (long_name, empty), (illegal_name, empty)], path
|
|
)
|
|
|
|
# assert — three distinct, Excel-legal sheet names.
|
|
names = load_workbook(path).sheetnames
|
|
assert len(names) == 3
|
|
assert len(set(names)) == 3
|
|
assert all(len(name) <= 31 for name in names)
|
|
assert not any(ch in name for name in names for ch in r":/\?*[]")
|
|
|
|
|
|
def test_brands_the_header_row_and_freezes_it(tmp_path: Path) -> None:
|
|
# arrange
|
|
sheet = ExportSheet(
|
|
columns=("property_id", "loft_insulation"),
|
|
rows=({"property_id": 1, "loft_insulation": 1200.0},),
|
|
)
|
|
|
|
# act
|
|
path = str(tmp_path / "export.xlsx")
|
|
render_workbook([("Fabric first", sheet)], path)
|
|
|
|
# assert — the header is a bold, brand-filled band and the header row is
|
|
# frozen so it stays visible while scrolling.
|
|
worksheet = load_workbook(path)["Fabric first"]
|
|
header_cell = worksheet["A1"]
|
|
assert header_cell.font.bold is True
|
|
assert header_cell.fill.fgColor.rgb == HEADER_FILL
|
|
assert worksheet.freeze_panes == "A2"
|