386 lines
11 KiB
Python
386 lines
11 KiB
Python
# Copyright (c) 2010-2024 openpyxl
|
|
|
|
from openpyxl.descriptors.serialisable import Serialisable
|
|
from openpyxl.descriptors import (
|
|
Descriptor,
|
|
Alias,
|
|
Typed,
|
|
Bool,
|
|
Integer,
|
|
NoneSet,
|
|
String,
|
|
Sequence,
|
|
)
|
|
from openpyxl.descriptors.excel import ExtensionList, CellRange
|
|
from openpyxl.descriptors.sequence import NestedSequence
|
|
from openpyxl.xml.constants import SHEET_MAIN_NS, REL_NS
|
|
from openpyxl.xml.functions import tostring
|
|
from openpyxl.utils import range_boundaries
|
|
from openpyxl.utils.escape import escape, unescape
|
|
|
|
from .related import Related
|
|
|
|
from .filters import (
|
|
AutoFilter,
|
|
SortState,
|
|
)
|
|
|
|
TABLESTYLES = tuple(
|
|
["TableStyleMedium{0}".format(i) for i in range(1, 29)]
|
|
+ ["TableStyleLight{0}".format(i) for i in range(1, 22)]
|
|
+ ["TableStyleDark{0}".format(i) for i in range(1, 12)]
|
|
)
|
|
|
|
PIVOTSTYLES = tuple(
|
|
["PivotStyleMedium{0}".format(i) for i in range(1, 29)]
|
|
+ ["PivotStyleLight{0}".format(i) for i in range(1, 29)]
|
|
+ ["PivotStyleDark{0}".format(i) for i in range(1, 29)]
|
|
)
|
|
|
|
|
|
class TableStyleInfo(Serialisable):
|
|
|
|
tagname = "tableStyleInfo"
|
|
|
|
name = String(allow_none=True)
|
|
showFirstColumn = Bool(allow_none=True)
|
|
showLastColumn = Bool(allow_none=True)
|
|
showRowStripes = Bool(allow_none=True)
|
|
showColumnStripes = Bool(allow_none=True)
|
|
|
|
def __init__(self,
|
|
name=None,
|
|
showFirstColumn=None,
|
|
showLastColumn=None,
|
|
showRowStripes=None,
|
|
showColumnStripes=None,
|
|
):
|
|
self.name = name
|
|
self.showFirstColumn = showFirstColumn
|
|
self.showLastColumn = showLastColumn
|
|
self.showRowStripes = showRowStripes
|
|
self.showColumnStripes = showColumnStripes
|
|
|
|
|
|
class XMLColumnProps(Serialisable):
|
|
|
|
tagname = "xmlColumnPr"
|
|
|
|
mapId = Integer()
|
|
xpath = String()
|
|
denormalized = Bool(allow_none=True)
|
|
xmlDataType = String()
|
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
|
|
|
__elements__ = ()
|
|
|
|
def __init__(self,
|
|
mapId=None,
|
|
xpath=None,
|
|
denormalized=None,
|
|
xmlDataType=None,
|
|
extLst=None,
|
|
):
|
|
self.mapId = mapId
|
|
self.xpath = xpath
|
|
self.denormalized = denormalized
|
|
self.xmlDataType = xmlDataType
|
|
|
|
|
|
class TableFormula(Serialisable):
|
|
|
|
tagname = "tableFormula"
|
|
|
|
## Note formula is stored as the text value
|
|
|
|
array = Bool(allow_none=True)
|
|
attr_text = Descriptor()
|
|
text = Alias('attr_text')
|
|
|
|
|
|
def __init__(self,
|
|
array=None,
|
|
attr_text=None,
|
|
):
|
|
self.array = array
|
|
self.attr_text = attr_text
|
|
|
|
|
|
class TableColumn(Serialisable):
|
|
|
|
tagname = "tableColumn"
|
|
|
|
id = Integer()
|
|
uniqueName = String(allow_none=True)
|
|
name = String()
|
|
totalsRowFunction = NoneSet(values=(['sum', 'min', 'max', 'average',
|
|
'count', 'countNums', 'stdDev', 'var', 'custom']))
|
|
totalsRowLabel = String(allow_none=True)
|
|
queryTableFieldId = Integer(allow_none=True)
|
|
headerRowDxfId = Integer(allow_none=True)
|
|
dataDxfId = Integer(allow_none=True)
|
|
totalsRowDxfId = Integer(allow_none=True)
|
|
headerRowCellStyle = String(allow_none=True)
|
|
dataCellStyle = String(allow_none=True)
|
|
totalsRowCellStyle = String(allow_none=True)
|
|
calculatedColumnFormula = Typed(expected_type=TableFormula, allow_none=True)
|
|
totalsRowFormula = Typed(expected_type=TableFormula, allow_none=True)
|
|
xmlColumnPr = Typed(expected_type=XMLColumnProps, allow_none=True)
|
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
|
|
|
__elements__ = ('calculatedColumnFormula', 'totalsRowFormula',
|
|
'xmlColumnPr', 'extLst')
|
|
|
|
def __init__(self,
|
|
id=None,
|
|
uniqueName=None,
|
|
name=None,
|
|
totalsRowFunction=None,
|
|
totalsRowLabel=None,
|
|
queryTableFieldId=None,
|
|
headerRowDxfId=None,
|
|
dataDxfId=None,
|
|
totalsRowDxfId=None,
|
|
headerRowCellStyle=None,
|
|
dataCellStyle=None,
|
|
totalsRowCellStyle=None,
|
|
calculatedColumnFormula=None,
|
|
totalsRowFormula=None,
|
|
xmlColumnPr=None,
|
|
extLst=None,
|
|
):
|
|
self.id = id
|
|
self.uniqueName = uniqueName
|
|
self.name = name
|
|
self.totalsRowFunction = totalsRowFunction
|
|
self.totalsRowLabel = totalsRowLabel
|
|
self.queryTableFieldId = queryTableFieldId
|
|
self.headerRowDxfId = headerRowDxfId
|
|
self.dataDxfId = dataDxfId
|
|
self.totalsRowDxfId = totalsRowDxfId
|
|
self.headerRowCellStyle = headerRowCellStyle
|
|
self.dataCellStyle = dataCellStyle
|
|
self.totalsRowCellStyle = totalsRowCellStyle
|
|
self.calculatedColumnFormula = calculatedColumnFormula
|
|
self.totalsRowFormula = totalsRowFormula
|
|
self.xmlColumnPr = xmlColumnPr
|
|
self.extLst = extLst
|
|
|
|
|
|
def __iter__(self):
|
|
for k, v in super().__iter__():
|
|
if k == 'name':
|
|
v = escape(v)
|
|
yield k, v
|
|
|
|
|
|
@classmethod
|
|
def from_tree(cls, node):
|
|
self = super().from_tree(node)
|
|
self.name = unescape(self.name)
|
|
return self
|
|
|
|
|
|
class TableNameDescriptor(String):
|
|
|
|
"""
|
|
Table names cannot have spaces in them
|
|
"""
|
|
|
|
def __set__(self, instance, value):
|
|
if value is not None and " " in value:
|
|
raise ValueError("Table names cannot have spaces")
|
|
super().__set__(instance, value)
|
|
|
|
|
|
class Table(Serialisable):
|
|
|
|
_path = "/tables/table{0}.xml"
|
|
mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
|
|
_rel_type = REL_NS + "/table"
|
|
_rel_id = None
|
|
|
|
tagname = "table"
|
|
|
|
id = Integer()
|
|
name = String(allow_none=True)
|
|
displayName = TableNameDescriptor()
|
|
comment = String(allow_none=True)
|
|
ref = CellRange()
|
|
tableType = NoneSet(values=(['worksheet', 'xml', 'queryTable']))
|
|
headerRowCount = Integer(allow_none=True)
|
|
insertRow = Bool(allow_none=True)
|
|
insertRowShift = Bool(allow_none=True)
|
|
totalsRowCount = Integer(allow_none=True)
|
|
totalsRowShown = Bool(allow_none=True)
|
|
published = Bool(allow_none=True)
|
|
headerRowDxfId = Integer(allow_none=True)
|
|
dataDxfId = Integer(allow_none=True)
|
|
totalsRowDxfId = Integer(allow_none=True)
|
|
headerRowBorderDxfId = Integer(allow_none=True)
|
|
tableBorderDxfId = Integer(allow_none=True)
|
|
totalsRowBorderDxfId = Integer(allow_none=True)
|
|
headerRowCellStyle = String(allow_none=True)
|
|
dataCellStyle = String(allow_none=True)
|
|
totalsRowCellStyle = String(allow_none=True)
|
|
connectionId = Integer(allow_none=True)
|
|
autoFilter = Typed(expected_type=AutoFilter, allow_none=True)
|
|
sortState = Typed(expected_type=SortState, allow_none=True)
|
|
tableColumns = NestedSequence(expected_type=TableColumn, count=True)
|
|
tableStyleInfo = Typed(expected_type=TableStyleInfo, allow_none=True)
|
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
|
|
|
__elements__ = ('autoFilter', 'sortState', 'tableColumns',
|
|
'tableStyleInfo')
|
|
|
|
def __init__(self,
|
|
id=1,
|
|
displayName=None,
|
|
ref=None,
|
|
name=None,
|
|
comment=None,
|
|
tableType=None,
|
|
headerRowCount=1,
|
|
insertRow=None,
|
|
insertRowShift=None,
|
|
totalsRowCount=None,
|
|
totalsRowShown=None,
|
|
published=None,
|
|
headerRowDxfId=None,
|
|
dataDxfId=None,
|
|
totalsRowDxfId=None,
|
|
headerRowBorderDxfId=None,
|
|
tableBorderDxfId=None,
|
|
totalsRowBorderDxfId=None,
|
|
headerRowCellStyle=None,
|
|
dataCellStyle=None,
|
|
totalsRowCellStyle=None,
|
|
connectionId=None,
|
|
autoFilter=None,
|
|
sortState=None,
|
|
tableColumns=(),
|
|
tableStyleInfo=None,
|
|
extLst=None,
|
|
):
|
|
self.id = id
|
|
self.displayName = displayName
|
|
if name is None:
|
|
name = displayName
|
|
self.name = name
|
|
self.comment = comment
|
|
self.ref = ref
|
|
self.tableType = tableType
|
|
self.headerRowCount = headerRowCount
|
|
self.insertRow = insertRow
|
|
self.insertRowShift = insertRowShift
|
|
self.totalsRowCount = totalsRowCount
|
|
self.totalsRowShown = totalsRowShown
|
|
self.published = published
|
|
self.headerRowDxfId = headerRowDxfId
|
|
self.dataDxfId = dataDxfId
|
|
self.totalsRowDxfId = totalsRowDxfId
|
|
self.headerRowBorderDxfId = headerRowBorderDxfId
|
|
self.tableBorderDxfId = tableBorderDxfId
|
|
self.totalsRowBorderDxfId = totalsRowBorderDxfId
|
|
self.headerRowCellStyle = headerRowCellStyle
|
|
self.dataCellStyle = dataCellStyle
|
|
self.totalsRowCellStyle = totalsRowCellStyle
|
|
self.connectionId = connectionId
|
|
self.autoFilter = autoFilter
|
|
self.sortState = sortState
|
|
self.tableColumns = tableColumns
|
|
self.tableStyleInfo = tableStyleInfo
|
|
|
|
|
|
def to_tree(self):
|
|
tree = super().to_tree()
|
|
tree.set("xmlns", SHEET_MAIN_NS)
|
|
return tree
|
|
|
|
|
|
@property
|
|
def path(self):
|
|
"""
|
|
Return path within the archive
|
|
"""
|
|
return "/xl" + self._path.format(self.id)
|
|
|
|
|
|
def _write(self, archive):
|
|
"""
|
|
Serialise to XML and write to archive
|
|
"""
|
|
xml = self.to_tree()
|
|
archive.writestr(self.path[1:], tostring(xml))
|
|
|
|
|
|
def _initialise_columns(self):
|
|
"""
|
|
Create a list of table columns from a cell range
|
|
Always set a ref if we have headers (the default)
|
|
Column headings must be strings and must match cells in the worksheet.
|
|
"""
|
|
|
|
min_col, min_row, max_col, max_row = range_boundaries(self.ref)
|
|
for idx in range(min_col, max_col+1):
|
|
col = TableColumn(id=idx, name="Column{0}".format(idx))
|
|
self.tableColumns.append(col)
|
|
if self.headerRowCount and not self.autoFilter:
|
|
self.autoFilter = AutoFilter(ref=self.ref)
|
|
|
|
|
|
@property
|
|
def column_names(self):
|
|
return [column.name for column in self.tableColumns]
|
|
|
|
|
|
class TablePartList(Serialisable):
|
|
|
|
tagname = "tableParts"
|
|
|
|
count = Integer(allow_none=True)
|
|
tablePart = Sequence(expected_type=Related)
|
|
|
|
__elements__ = ('tablePart',)
|
|
__attrs__ = ('count',)
|
|
|
|
def __init__(self,
|
|
count=None,
|
|
tablePart=(),
|
|
):
|
|
self.tablePart = tablePart
|
|
|
|
|
|
def append(self, part):
|
|
self.tablePart.append(part)
|
|
|
|
|
|
@property
|
|
def count(self):
|
|
return len(self.tablePart)
|
|
|
|
|
|
def __bool__(self):
|
|
return bool(self.tablePart)
|
|
|
|
|
|
class TableList(dict):
|
|
|
|
|
|
def add(self, table):
|
|
if not isinstance(table, Table):
|
|
raise TypeError("You can only add tables")
|
|
self[table.name] = table
|
|
|
|
|
|
def get(self, name=None, table_range=None):
|
|
if name is not None:
|
|
return super().get(name)
|
|
for table in self.values():
|
|
if table_range == table.ref:
|
|
return table
|
|
|
|
|
|
def items(self):
|
|
return [(name, table.ref) for name, table in super().items()]
|