feat(xlsx): expose a guarded worksheet-at-a-time workbook reader
What
Adds a public way to read a workbook one worksheet at a time with the same protections partition_xlsx() uses:
from unstructured.partition.xlsx import open_excel_workbook
with open_excel_workbook(file_bytes, compact_empty_rows=True) as workbook:
for name in workbook.sheet_names:
frame = workbook.parse(name, header=None)
... # release `frame` before parsing the next sheet
open_excel_workbook():
- rejects non-Office and password-protected files with
UnprocessableEntityError, aspartition_xlsx()does; - picks the engine from the file signature (
openpyxlfor XLSX,xlrdfor XLS) and rejects anything else withUnprocessableEntityError; - runs the existing
XLSX_MAX_CELLSworksheet cell-count check before any worksheet is read; - optionally reads with runs of empty rows compacted (XLSX only), as subtable detection already does;
- closes the reader when the block exits, including on errors.
partition_xlsx() now reads through the same function, so its behavior is unchanged.
Why
Integrations that need their own per-worksheet processing currently have two options: call pandas.read_excel directly, which skips the cell-count limit and reads every sheet into memory at once, or import private helpers (_check_worksheet_cell_count, _RowCompactingOpenpyxlReader) that can change without notice. A tiny file with one far-away cell can still expand into a huge dense data-frame through the first route. This gives such callers a supported, guarded entry point.
Validation
- New
test_xlsx_reader.py: engine selection, password-protected rejection, cell-limit enforcement before reading (independent of the ambientXLSX_MAX_CELLS), empty-row compaction, repeated worksheet reads, and reader cleanup on success and on a consumer error. - XLSX partitioner suite passes unchanged; baseline-vs-branch workbook comparisons produce identical elements.
- Ruff check/format pass.
Release
Targets 0.27.32 as 0.27.32-dev5 (0.27.31 was taken by #4538, which merged first.) Released together with #4419, #4516, #4517, #4524 and #4523, and followed by the stacked dense-worksheet PR. Whichever merges after another rebases and renumbers the CHANGELOG heading, as usual for this repo.