"""Минимальный сборщик xlsx в памяти для тестов (без openpyxl). Поддерживает: shared strings (включая красные ранки), цифры, мержи, скрытые столбцы. Формат достаточно строгий, чтобы покрывать реальный парсер (он читает именно этот поднабор OOXML). """ from __future__ import annotations import io import zipfile from dataclasses import dataclass, field from xml.sax.saxutils import escape def col_letter(idx: int) -> str: """0-based индекс столбца -> 'A', 'B', ..., 'AA'.""" s = "" idx += 1 while idx: idx, rem = divmod(idx - 1, 26) s = chr(ord("A") + rem) + s return s @dataclass class SheetSpec: rows: list[list] merges: list[str] = field(default_factory=list) hidden_cols: list[tuple[int, int]] = field(default_factory=list) # 0-based, инклязивно hidden_rows: list[int] = field(default_factory=list) # 0-based def build_workbook(sheets: dict[str, SheetSpec]) -> bytes: shared: list[str] = [] shared_index: dict[str, int] = {} red_runs: dict[int, list[tuple[str, bool]]] = {} def intern(value) -> tuple[str, str, object]: """-> (тип ячейки, атрибут t, значение v / None)""" if value is None or value == "": return None, "", None if isinstance(value, (int, float)): return "n", "", str(value) if isinstance(value, list): idx = len(shared) shared.append(None) red_runs[idx] = value return "s", "s", idx idx = shared_index.get(value) if idx is None: idx = len(shared) shared_index[value] = idx shared.append(value) return "s", "s", idx sheet_xml: dict[str, str] = {} rels = [] sheet_defs = [] for i, (name, spec) in enumerate(sheets.items(), start=1): rel_id = f"rId{i}" rels.append( f'' ) sheet_defs.append( f'' ) cols = "".join( f'' for a, b in spec.hidden_cols ) cols_xml = f"{cols}" if cols else "" row_xml = [] for r, row in enumerate(spec.rows, start=1): cells = [] for c, value in enumerate(row): kind, t_attr, v = intern(value) if kind is None: continue t = f' t="{t_attr}"' if t_attr else "" cells.append(f'{escape(str(v))}') if cells: hidden = ' hidden="1"' if (r - 1) in spec.hidden_rows else "" row_xml.append(f'{"".join(cells)}') merges_xml = "" if spec.merges: mc = "".join(f'' for m in spec.merges) merges_xml = f'{mc}' sheet_xml[f"sheet{i}.xml"] = ( '' '' f"{cols_xml}{''.join(row_xml)}{merges_xml}" "" ) sst_parts = [] for i, value in enumerate(shared): if i in red_runs: runs = "".join( f'{("" if red else "")}' f"{escape(text)}" for text, red in red_runs[i] ) sst_parts.append(f"{runs}") else: sst_parts.append(f"{escape(value)}") sst = ( '' '' f"{''.join(sst_parts)}" ) content_types = ["", "", ''] for i in range(1, len(sheets) + 1): content_types.append( f'' ) if shared: content_types.append( '' ) buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: zf.writestr( "[Content_Types].xml", '' '' + "".join(content_types) + "", ) zf.writestr( "_rels/.rels", '' '' '' "", ) zf.writestr( "xl/workbook.xml", '' '' f"{''.join(sheet_defs)}", ) zf.writestr( "xl/_rels/workbook.xml.rels", '' '' + "".join(rels) + "", ) for fname, xml in sheet_xml.items(): zf.writestr(f"xl/worksheets/{fname}", xml) if shared: zf.writestr("xl/sharedStrings.xml", sst) return buf.getvalue()