Module refinery.lib.excel.workbook
Expand source code Browse git
from __future__ import annotations
import enum
import zipfile
import zlib
from abc import ABC, abstractmethod
from collections.abc import Sequence
from typing import IO, Iterator
from defusedxml.ElementTree import fromstring
from refinery.lib.excel.common import (
XML_DEFECTS,
Cell,
DefinedName,
ExcelFormatError,
FormulaSource,
SheetKind,
local_name,
)
from refinery.lib.excel.formula.model import Expression
from refinery.lib.id import Fmt, get_microsoft_format
from refinery.lib.structures import MemoryFile
class ExcelFormat(enum.Enum):
"""
The container family of an Excel workbook.
"""
OOXML = enum.auto()
XLSB = enum.auto()
BIFF = enum.auto()
class ExcelSheet(ABC):
"""
A single sheet of an `ExcelWorkbook`.
"""
name: str
kind: SheetKind
@abstractmethod
def cells(self) -> Iterator[Cell]:
"""
Iterate over every cell of the sheet, in document order.
"""
class ExcelWorkbook(ABC):
"""
A workbook of arbitrary Excel format, exposing its sheets without interpreting or
rendering any of the values they contain.
"""
format: ExcelFormat
#: Whether the serial date numbers of this workbook count from the 1904 epoch.
date_mode_1904: bool
@abstractmethod
def sheets(self) -> Sequence[ExcelSheet]:
"""
All sheets of the workbook in document order.
"""
def sheet_index(self, name: str) -> int | None:
"""
The position of a sheet in the sequence the format's defined-name scopes count: the
full sheet table of the workbook, chartsheets included, matched case-insensitively as
Excel matches sheet names. `None` when the workbook has no such sheet.
"""
key = name.lower()
for index, sheet in enumerate(self.sheets()):
if sheet.name.lower() == key:
return index
return None
def formula(self, source: FormulaSource) -> Expression | None:
"""
Decode the formula source of a cell or a defined name of this workbook into the
formula model. A source the format cannot decode yields the unparsed carrier rather
than an error, and `None` — a cell that is not a formula — yields `None`.
"""
raise NotImplementedError
def defined_names(self) -> Sequence[DefinedName]:
"""
All defined names of the workbook, in the order their records appear; the position
of a name in this list is the index its format's name token carries.
"""
raise NotImplementedError
_RAW_BIFF_VERSIONS = b'\x00\x02\x04\x08'
_RAW_BIFF_LENGTHS = frozenset((0x0004, 0x0006, 0x0008, 0x0010))
_PACKAGE_DEFECTS = (zipfile.BadZipFile, zlib.error, EOFError, RuntimeError)
class _PartStream:
"""
A package part as a read-only binary stream. Reading a defective entry of the archive raises
`refinery.lib.excel.common.ExcelFormatError` instead of the exception class of the `zipfile`
module.
"""
def __init__(self, stream: IO[bytes]):
self._stream = stream
def read(self, size: int = -1) -> bytes:
try:
return self._stream.read(size)
except _PACKAGE_DEFECTS as error:
raise ExcelFormatError('the package contains a defective part') from error
class _Package:
"""
The zip container of an OOXML or XLSB workbook. Parts are looked up case-insensitively
and with normalized slashes because workbooks written by non-Microsoft software
frequently deviate from the conventional part names. A part that is stored under the
same name more than once resolves to its last entry, as it does for `zipfile`.
"""
def __init__(self, data: bytes | bytearray | memoryview):
self._zip = zipfile.ZipFile(MemoryFile(data, bytes))
self._entries: dict[str, zipfile.ZipInfo] = {}
for info in self._zip.infolist():
if not info.is_dir():
self._entries[self._key(info.filename)] = info
@staticmethod
def _key(name: str) -> str:
return name.replace('\\', '/').lower()
def __contains__(self, part: str) -> bool:
return self._key(part) in self._entries
def open(self, part: str) -> _PartStream | None:
info = self._entries.get(self._key(part))
if info is None:
return None
try:
return _PartStream(self._zip.open(info))
except _PACKAGE_DEFECTS as error:
raise ExcelFormatError('the package contains a defective part') from error
def detect_format(data: bytes | bytearray | memoryview) -> ExcelFormat | None:
"""
Determine the Excel container family of the input: a zip archive containing an OOXML
workbook part, a zip archive containing an XLSB workbook part, an OLE compound file with
a workbook stream, or a raw BIFF record stream that is not wrapped in any container.
The zip detection accepts a part at its conventional location or one that the content
types of the package declare as the main document part, which is exactly what the readers
resolve; any other zip is not a workbook.
"""
view = memoryview(data)
if view[:2] == b'PK':
try:
package = _Package(view)
except zipfile.BadZipFile:
return None
if 'xl/workbook.xml' in package:
return ExcelFormat.OOXML
if 'xl/workbook.bin' in package:
return ExcelFormat.XLSB
try:
content_types = package.open('[Content_Types].xml')
except ExcelFormatError:
return None
if content_types is not None:
try:
root = fromstring(content_types.read())
except (*XML_DEFECTS, ExcelFormatError):
return None
for element in root.iter():
if (
local_name(element.tag) == 'Override'
and 'sheet.main+xml' in (element.get('ContentType') or '')
):
return ExcelFormat.OOXML
return None
if get_microsoft_format(view) == Fmt.XLS:
return ExcelFormat.BIFF
if (
view[:1] == b'\x09'
and view[1:2] in _RAW_BIFF_VERSIONS
and int.from_bytes(view[2:4], 'little') in _RAW_BIFF_LENGTHS
):
return ExcelFormat.BIFF
return None
def open_workbook(data: bytes | bytearray | memoryview) -> ExcelWorkbook:
"""
Read a workbook of any supported Excel format. Raises `ExcelFormatError` when the input
is not a workbook at all; sheets that are defective are reported while iterating their
cells rather than when opening the workbook.
"""
detected = detect_format(data)
if detected is ExcelFormat.OOXML:
from refinery.lib.excel.ooxml import OoxmlWorkbook
return OoxmlWorkbook(data)
if detected is ExcelFormat.BIFF:
from refinery.lib.excel.biff import BiffWorkbook
return BiffWorkbook(data)
if detected is ExcelFormat.XLSB:
from refinery.lib.excel.xlsb import XlsbWorkbook
return XlsbWorkbook(data)
if detected is None:
raise ExcelFormatError('Input not recognized as Excel workbook.')
raise ExcelFormatError(F'The {detected.name} format is not implemented yet.')
Functions
def detect_format(data)-
Determine the Excel container family of the input: a zip archive containing an OOXML workbook part, a zip archive containing an XLSB workbook part, an OLE compound file with a workbook stream, or a raw BIFF record stream that is not wrapped in any container. The zip detection accepts a part at its conventional location or one that the content types of the package declare as the main document part, which is exactly what the readers resolve; any other zip is not a workbook.
Expand source code Browse git
def detect_format(data: bytes | bytearray | memoryview) -> ExcelFormat | None: """ Determine the Excel container family of the input: a zip archive containing an OOXML workbook part, a zip archive containing an XLSB workbook part, an OLE compound file with a workbook stream, or a raw BIFF record stream that is not wrapped in any container. The zip detection accepts a part at its conventional location or one that the content types of the package declare as the main document part, which is exactly what the readers resolve; any other zip is not a workbook. """ view = memoryview(data) if view[:2] == b'PK': try: package = _Package(view) except zipfile.BadZipFile: return None if 'xl/workbook.xml' in package: return ExcelFormat.OOXML if 'xl/workbook.bin' in package: return ExcelFormat.XLSB try: content_types = package.open('[Content_Types].xml') except ExcelFormatError: return None if content_types is not None: try: root = fromstring(content_types.read()) except (*XML_DEFECTS, ExcelFormatError): return None for element in root.iter(): if ( local_name(element.tag) == 'Override' and 'sheet.main+xml' in (element.get('ContentType') or '') ): return ExcelFormat.OOXML return None if get_microsoft_format(view) == Fmt.XLS: return ExcelFormat.BIFF if ( view[:1] == b'\x09' and view[1:2] in _RAW_BIFF_VERSIONS and int.from_bytes(view[2:4], 'little') in _RAW_BIFF_LENGTHS ): return ExcelFormat.BIFF return None def open_workbook(data)-
Read a workbook of any supported Excel format. Raises
ExcelFormatErrorwhen the input is not a workbook at all; sheets that are defective are reported while iterating their cells rather than when opening the workbook.Expand source code Browse git
def open_workbook(data: bytes | bytearray | memoryview) -> ExcelWorkbook: """ Read a workbook of any supported Excel format. Raises `ExcelFormatError` when the input is not a workbook at all; sheets that are defective are reported while iterating their cells rather than when opening the workbook. """ detected = detect_format(data) if detected is ExcelFormat.OOXML: from refinery.lib.excel.ooxml import OoxmlWorkbook return OoxmlWorkbook(data) if detected is ExcelFormat.BIFF: from refinery.lib.excel.biff import BiffWorkbook return BiffWorkbook(data) if detected is ExcelFormat.XLSB: from refinery.lib.excel.xlsb import XlsbWorkbook return XlsbWorkbook(data) if detected is None: raise ExcelFormatError('Input not recognized as Excel workbook.') raise ExcelFormatError(F'The {detected.name} format is not implemented yet.')
Classes
class ExcelFormat (*args, **kwds)-
The container family of an Excel workbook.
Expand source code Browse git
class ExcelFormat(enum.Enum): """ The container family of an Excel workbook. """ OOXML = enum.auto() XLSB = enum.auto() BIFF = enum.auto()Ancestors
- enum.Enum
Class variables
var OOXML-
The type of the None singleton.
var XLSB-
The type of the None singleton.
var BIFF-
The type of the None singleton.
class ExcelSheet-
A single sheet of an
ExcelWorkbook.Expand source code Browse git
class ExcelSheet(ABC): """ A single sheet of an `ExcelWorkbook`. """ name: str kind: SheetKind @abstractmethod def cells(self) -> Iterator[Cell]: """ Iterate over every cell of the sheet, in document order. """Ancestors
- abc.ABC
Subclasses
Class variables
var name-
The type of the None singleton.
var kind-
The type of the None singleton.
Methods
def cells(self)-
Iterate over every cell of the sheet, in document order.
Expand source code Browse git
@abstractmethod def cells(self) -> Iterator[Cell]: """ Iterate over every cell of the sheet, in document order. """
class ExcelWorkbook-
A workbook of arbitrary Excel format, exposing its sheets without interpreting or rendering any of the values they contain.
Expand source code Browse git
class ExcelWorkbook(ABC): """ A workbook of arbitrary Excel format, exposing its sheets without interpreting or rendering any of the values they contain. """ format: ExcelFormat #: Whether the serial date numbers of this workbook count from the 1904 epoch. date_mode_1904: bool @abstractmethod def sheets(self) -> Sequence[ExcelSheet]: """ All sheets of the workbook in document order. """ def sheet_index(self, name: str) -> int | None: """ The position of a sheet in the sequence the format's defined-name scopes count: the full sheet table of the workbook, chartsheets included, matched case-insensitively as Excel matches sheet names. `None` when the workbook has no such sheet. """ key = name.lower() for index, sheet in enumerate(self.sheets()): if sheet.name.lower() == key: return index return None def formula(self, source: FormulaSource) -> Expression | None: """ Decode the formula source of a cell or a defined name of this workbook into the formula model. A source the format cannot decode yields the unparsed carrier rather than an error, and `None` — a cell that is not a formula — yields `None`. """ raise NotImplementedError def defined_names(self) -> Sequence[DefinedName]: """ All defined names of the workbook, in the order their records appear; the position of a name in this list is the index its format's name token carries. """ raise NotImplementedErrorAncestors
- abc.ABC
Subclasses
Class variables
var format-
The type of the None singleton.
var date_mode_1904-
Whether the serial date numbers of this workbook count from the 1904 epoch.
Methods
def sheets(self)-
All sheets of the workbook in document order.
Expand source code Browse git
@abstractmethod def sheets(self) -> Sequence[ExcelSheet]: """ All sheets of the workbook in document order. """ def sheet_index(self, name)-
The position of a sheet in the sequence the format's defined-name scopes count: the full sheet table of the workbook, chartsheets included, matched case-insensitively as Excel matches sheet names.
Nonewhen the workbook has no such sheet.Expand source code Browse git
def sheet_index(self, name: str) -> int | None: """ The position of a sheet in the sequence the format's defined-name scopes count: the full sheet table of the workbook, chartsheets included, matched case-insensitively as Excel matches sheet names. `None` when the workbook has no such sheet. """ key = name.lower() for index, sheet in enumerate(self.sheets()): if sheet.name.lower() == key: return index return None def formula(self, source)-
Decode the formula source of a cell or a defined name of this workbook into the formula model. A source the format cannot decode yields the unparsed carrier rather than an error, and
None— a cell that is not a formula — yieldsNone.Expand source code Browse git
def formula(self, source: FormulaSource) -> Expression | None: """ Decode the formula source of a cell or a defined name of this workbook into the formula model. A source the format cannot decode yields the unparsed carrier rather than an error, and `None` — a cell that is not a formula — yields `None`. """ raise NotImplementedError def defined_names(self)-
All defined names of the workbook, in the order their records appear; the position of a name in this list is the index its format's name token carries.
Expand source code Browse git
def defined_names(self) -> Sequence[DefinedName]: """ All defined names of the workbook, in the order their records appear; the position of a name in this list is the index its format's name token carries. """ raise NotImplementedError