2024-12-04 13:35:57 +05:00

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()]