- .forgejo/workflows/test.yaml: offline tests on Python 3.10 and 3.13 - LICENSE: Target Revenue Source License V1C1 (org-wide preliminary licence) - pyproject: license field; AGENTS/SCOPE: CI and licence facts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 153021@bnt-lap001 Assistant-Session: 46917be4-6400-4106-b8b7-61953a0caf24
53 lines
2 KiB
Python
53 lines
2 KiB
Python
"""Minimal read-only XLSX reader (stdlib only): sheet -> list of rows of strings."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import re
|
|
import zipfile
|
|
import xml.etree.ElementTree as ET
|
|
|
|
_NS = {"m": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
|
|
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
|
|
_T = "{%s}t" % _NS["m"]
|
|
|
|
|
|
def _col_index(ref: str) -> int:
|
|
letters = re.match(r"[A-Z]+", ref).group(0)
|
|
n = 0
|
|
for ch in letters:
|
|
n = n * 26 + (ord(ch) - 64)
|
|
return n - 1
|
|
|
|
|
|
def read_sheet(data: bytes, sheet: str | int = 0) -> list[list[str]]:
|
|
z = zipfile.ZipFile(io.BytesIO(data))
|
|
shared: list[str] = []
|
|
if "xl/sharedStrings.xml" in z.namelist():
|
|
for si in ET.fromstring(z.read("xl/sharedStrings.xml")).findall("m:si", _NS):
|
|
shared.append("".join(t.text or "" for t in si.iter(_T)))
|
|
wb = ET.fromstring(z.read("xl/workbook.xml"))
|
|
sheets = [s for s in wb.find("m:sheets", _NS)]
|
|
if isinstance(sheet, str):
|
|
idx = next(i for i, s in enumerate(sheets) if s.get("name") == sheet)
|
|
else:
|
|
idx = sheet
|
|
rid = sheets[idx].get("{%s}id" % _NS["r"])
|
|
rels = ET.fromstring(z.read("xl/_rels/workbook.xml.rels"))
|
|
target = next(r.get("Target") for r in rels if r.get("Id") == rid)
|
|
path = "xl/" + target.lstrip("/").removeprefix("xl/")
|
|
root = ET.fromstring(z.read(path))
|
|
rows: list[list[str]] = []
|
|
for r in root.find("m:sheetData", _NS).findall("m:row", _NS):
|
|
cells: dict[int, str] = {}
|
|
for c in r.findall("m:c", _NS):
|
|
v = c.find("m:v", _NS)
|
|
if v is None:
|
|
inline = c.find("m:is", _NS)
|
|
text = "".join(t.text or "" for t in inline.iter(_T)) if inline is not None else ""
|
|
else:
|
|
text = shared[int(v.text)] if c.get("t") == "s" else (v.text or "")
|
|
cells[_col_index(c.get("r"))] = text
|
|
width = max(cells) + 1 if cells else 0
|
|
rows.append([cells.get(i, "") for i in range(width)])
|
|
return rows
|