pyfastexcel 0.1.8__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyfastexcel/__init__.py +21 -0
- pyfastexcel/_typing.py +31 -0
- pyfastexcel/chart.py +346 -0
- pyfastexcel/driver.py +290 -0
- pyfastexcel/enums.py +119 -0
- pyfastexcel/logformatter.py +38 -0
- pyfastexcel/pivot.py +124 -0
- pyfastexcel/pyfastexcel.dll +0 -0
- pyfastexcel/serializers.py +100 -0
- pyfastexcel/style.py +202 -0
- pyfastexcel/utils.py +184 -0
- pyfastexcel/validators.py +196 -0
- pyfastexcel/workbook.py +573 -0
- pyfastexcel/worksheet.py +1025 -0
- pyfastexcel/writer.py +148 -0
- pyfastexcel-0.1.8.dist-info/LICENSE +21 -0
- pyfastexcel-0.1.8.dist-info/METADATA +212 -0
- pyfastexcel-0.1.8.dist-info/RECORD +20 -0
- pyfastexcel-0.1.8.dist-info/WHEEL +5 -0
- pyfastexcel-0.1.8.dist-info/top_level.txt +1 -0
pyfastexcel/enums.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseEnum(Enum):
|
|
5
|
+
@classmethod
|
|
6
|
+
def get_enum(cls, name: str):
|
|
7
|
+
# Use casefold for case-insensitive comparison
|
|
8
|
+
name_casefolded = name.casefold()
|
|
9
|
+
for enum in cls:
|
|
10
|
+
if enum.name.casefold() == name_casefolded:
|
|
11
|
+
return enum
|
|
12
|
+
raise ValueError(f'{name} is not a valid {cls.__name__}')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ChartType(BaseEnum):
|
|
16
|
+
Area = 0
|
|
17
|
+
AreaStacked = 1
|
|
18
|
+
AreaPercentStacked = 2
|
|
19
|
+
Area3D = 3
|
|
20
|
+
Area3DStacked = 4
|
|
21
|
+
Area3DPercentStacked = 5
|
|
22
|
+
Bar = 6
|
|
23
|
+
BarStacked = 7
|
|
24
|
+
BarPercentStacked = 8
|
|
25
|
+
Bar3DClustered = 9
|
|
26
|
+
Bar3DStacked = 10
|
|
27
|
+
Bar3DPercentStacked = 11
|
|
28
|
+
Bar3DConeClustered = 12
|
|
29
|
+
Bar3DConeStacked = 13
|
|
30
|
+
Bar3DConePercentStacked = 14
|
|
31
|
+
Bar3DPyramidClustered = 15
|
|
32
|
+
Bar3DPyramidStacked = 16
|
|
33
|
+
Bar3DPyramidPercentStacked = 17
|
|
34
|
+
Bar3DCylinderClustered = 18
|
|
35
|
+
Bar3DCylinderStacked = 19
|
|
36
|
+
Bar3DCylinderPercentStacked = 20
|
|
37
|
+
Col = 21
|
|
38
|
+
ColStacked = 22
|
|
39
|
+
ColPercentStacked = 23
|
|
40
|
+
Col3D = 24
|
|
41
|
+
Col3DClustered = 25
|
|
42
|
+
Col3DStacked = 26
|
|
43
|
+
Col3DPercentStacked = 27
|
|
44
|
+
Col3DCone = 28
|
|
45
|
+
Col3DConeClustered = 29
|
|
46
|
+
Col3DConeStacked = 30
|
|
47
|
+
Col3DConePercentStacked = 31
|
|
48
|
+
Col3DPyramid = 32
|
|
49
|
+
Col3DPyramidClustered = 33
|
|
50
|
+
Col3DPyramidStacked = 34
|
|
51
|
+
Col3DPyramidPercentStacked = 35
|
|
52
|
+
Col3DCylinder = 36
|
|
53
|
+
Col3DCylinderClustered = 37
|
|
54
|
+
Col3DCylinderStacked = 38
|
|
55
|
+
Col3DCylinderPercentStacked = 39
|
|
56
|
+
Doughnut = 40
|
|
57
|
+
Line = 41
|
|
58
|
+
Line3D = 42
|
|
59
|
+
Pie = 43
|
|
60
|
+
Pie3D = 44
|
|
61
|
+
PieOfPie = 45
|
|
62
|
+
BarOfPie = 46
|
|
63
|
+
Radar = 47
|
|
64
|
+
Scatter = 48
|
|
65
|
+
Surface3D = 49
|
|
66
|
+
WireframeSurface3D = 50
|
|
67
|
+
Contour = 51
|
|
68
|
+
WireframeContour = 52
|
|
69
|
+
Bubble = 53
|
|
70
|
+
Bubble3D = 54
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ChartDataLabelPosition(BaseEnum):
|
|
74
|
+
Unset = 0
|
|
75
|
+
BestFit = 1
|
|
76
|
+
Below = 2
|
|
77
|
+
Center = 3
|
|
78
|
+
InsideBase = 4
|
|
79
|
+
InsideEnd = 5
|
|
80
|
+
Left = 6
|
|
81
|
+
OutsideEnd = 7
|
|
82
|
+
Right = 8
|
|
83
|
+
Above = 9
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ChartLineType(BaseEnum):
|
|
87
|
+
Unset = 0
|
|
88
|
+
Solid = 1
|
|
89
|
+
NONE = 2
|
|
90
|
+
Automatic = 3
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MarkerSymbol(BaseEnum):
|
|
94
|
+
Circle = 'circle'
|
|
95
|
+
Dash = 'dash'
|
|
96
|
+
Diamond = 'diamond'
|
|
97
|
+
Dot = 'dot'
|
|
98
|
+
NONE = 'none'
|
|
99
|
+
Picture = 'picture'
|
|
100
|
+
Plus = 'plus'
|
|
101
|
+
Square = 'square'
|
|
102
|
+
Star = 'star'
|
|
103
|
+
Triangle = 'triangle'
|
|
104
|
+
X = 'x'
|
|
105
|
+
Auto = 'auto'
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PivotSubTotal(BaseEnum):
|
|
109
|
+
Average = 'Average'
|
|
110
|
+
Count = 'Count'
|
|
111
|
+
CountNums = 'CountNums'
|
|
112
|
+
Max = 'Max'
|
|
113
|
+
Min = 'Min'
|
|
114
|
+
Product = 'Product'
|
|
115
|
+
StdDev = 'StdDev'
|
|
116
|
+
StdDevp = 'StdDevp'
|
|
117
|
+
Sum = 'Sum'
|
|
118
|
+
Var = 'Var'
|
|
119
|
+
Varp = 'Varp'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
NORMAL_FORMAT = '\033[%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s\033[0m'
|
|
5
|
+
LOG_COLORS = {
|
|
6
|
+
'DEBUG': '\033[90m', # Grey
|
|
7
|
+
'INFO': '\033[92m', # Green
|
|
8
|
+
'WARNING': '\033[93m', # Yellow
|
|
9
|
+
'ERROR': '\033[91m', # Red
|
|
10
|
+
'CRITICAL': '\033[91m', # Red
|
|
11
|
+
}
|
|
12
|
+
LOG_CACHE = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def custom_warning_format(message, *args):
|
|
16
|
+
start_color = '\033[93m'
|
|
17
|
+
reset_color = '\033[0m'
|
|
18
|
+
return f'{start_color}DeprecationWarning: {message}{reset_color}\n'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def log_warning(logger: logging.Logger, message: str):
|
|
22
|
+
if not LOG_CACHE.get(message):
|
|
23
|
+
logger.warning(message)
|
|
24
|
+
LOG_CACHE[message] = True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ColoredFormatter(logging.Formatter):
|
|
28
|
+
def __init__(self, fmt):
|
|
29
|
+
super().__init__(fmt)
|
|
30
|
+
|
|
31
|
+
def format(self, record):
|
|
32
|
+
record.log_color = LOG_COLORS[record.levelname]
|
|
33
|
+
return super().format(record)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
warnings.formatwarning = custom_warning_format
|
|
37
|
+
warnings.simplefilter('always', DeprecationWarning)
|
|
38
|
+
formatter = ColoredFormatter(NORMAL_FORMAT)
|
pyfastexcel/pivot.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, field_validator, field_serializer
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from .enums import PivotSubTotal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_pivot_table_style_config = ('PivotStyleLight', 'PivotStyleMedium', 'PivotStyleDark')
|
|
10
|
+
|
|
11
|
+
_pivot_table_style: set[str] = set()
|
|
12
|
+
_pivot_table_style.add('')
|
|
13
|
+
_pivot_table_style.add(None)
|
|
14
|
+
|
|
15
|
+
for name in _pivot_table_style_config:
|
|
16
|
+
for i in range(1, 29):
|
|
17
|
+
_pivot_table_style.add(f'{name}{i}')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PivotTableField(BaseModel):
|
|
21
|
+
"""
|
|
22
|
+
Model representing a field in a PivotTable.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
compact (Optional[bool]): Indicates whether the field is in compact form.
|
|
26
|
+
data (Optional[str]): Represents the data value for the field.
|
|
27
|
+
name (Optional[str]): The name of the field.
|
|
28
|
+
outline (Optional[bool]): Indicates whether the field is in outline form.
|
|
29
|
+
subtotal (Optional[str]): The type of subtotal for the field.
|
|
30
|
+
default_subtotal (Optional[bool]): Indicates whether this field has a default subtotal applied.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
compact: Optional[bool] = Field(None, serialization_alias='Compact')
|
|
34
|
+
data: Optional[str] = Field(None, serialization_alias='Data')
|
|
35
|
+
name: Optional[str] = Field(None, serialization_alias='Name')
|
|
36
|
+
outline: Optional[bool] = Field(None, serialization_alias='Outline')
|
|
37
|
+
subtotal: Optional[str | PivotSubTotal] = Field(None, serialization_alias='Subtotal')
|
|
38
|
+
default_subtotal: Optional[bool] = Field(None, serialization_alias='DefaultSubtotal')
|
|
39
|
+
|
|
40
|
+
@field_serializer('subtotal')
|
|
41
|
+
@classmethod
|
|
42
|
+
def subtotal_serializer(cls, subtotal: str | PivotSubTotal | None) -> str:
|
|
43
|
+
if subtotal is None:
|
|
44
|
+
return None
|
|
45
|
+
if isinstance(subtotal, PivotSubTotal):
|
|
46
|
+
return subtotal.value
|
|
47
|
+
return PivotSubTotal.get_enum(subtotal).value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PivotTable(BaseModel):
|
|
51
|
+
"""
|
|
52
|
+
Model representing a PivotTable, with its configuration and field settings.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
data_range (str): The range of data to be used in the pivot table, e.g., "Sheet1!A1:B2".
|
|
56
|
+
pivot_table_range (str): The range where the pivot table will be positioned, e.g., "Sheet1!C3:D4".
|
|
57
|
+
rows (list[PivotTableField]): List of fields used as rows in the pivot table.
|
|
58
|
+
pivot_filter ([PivotTableField]): List of fields used as filters in the pivot table.
|
|
59
|
+
columns (list[PivotTableField]): List of fields used as columns in the pivot table.
|
|
60
|
+
data (list[PivotTableField]): List of fields used as data fields in the pivot table.
|
|
61
|
+
row_grand_totals (Optional[bool Indicates whether to show row grand totals.
|
|
62
|
+
column_grand_totals (Optional[bool]): Indicates whether to show column grand.
|
|
63
|
+
show_drill (Optional[bool]): Indicates whether to show drill indicators.
|
|
64
|
+
show_row_headers (Optional[bool]): Indicates whether to show row headers.
|
|
65
|
+
show_column_headers (Optional[bool]): Indicates whether to show column headers.
|
|
66
|
+
show_row_stripes (Optional[bool]): Indicates whether to show row stripes.
|
|
67
|
+
show_col_stripes (Optional[bool]): Indicates whether to show column stripes.
|
|
68
|
+
show_last_column (Optional[bool]): Indicates whether to show the last column.
|
|
69
|
+
use_auto_formatting (Optional[bool]): Indicates whether to use automatic formatting.
|
|
70
|
+
page_over_then_down (Optional[bool]): Indicates whether pages should be ordered from top to bottom
|
|
71
|
+
then left to right.
|
|
72
|
+
merge_item (Optional[bool]): Indicates whether to merge items.
|
|
73
|
+
compact_data (Optional[bool]): Indicates whether to show in a compact form.
|
|
74
|
+
show_error (Optional[bool]): Indicates whether to show errors.
|
|
75
|
+
classic_layout (Optional[bool]): Indicates whether to use classic layout.
|
|
76
|
+
pivot_table_style_name (Optional[str]): Specifies the style the pivot table.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
data_range: str = Field(..., serialization_alias='DataRange')
|
|
80
|
+
pivot_table_range: str = Field(..., serialization_alias='PivotTableRange')
|
|
81
|
+
rows: list[PivotTableField] = Field([PivotTableField()], serialization_alias='Rows')
|
|
82
|
+
pivot_filter: list[PivotTableField] = Field([PivotTableField()], serialization_alias='Filter')
|
|
83
|
+
columns: list[PivotTableField] = Field([PivotTableField()], serialization_alias='Columns')
|
|
84
|
+
data: list[PivotTableField] = Field([PivotTableField()], serialization_alias='Data')
|
|
85
|
+
row_grand_totals: Optional[bool] = Field(None, serialization_alias='RowGrandTotals')
|
|
86
|
+
column_grand_totals: Optional[bool] = Field(None, serialization_alias='ColGrandTotals')
|
|
87
|
+
show_drill: Optional[bool] = Field(None, serialization_alias='ShowDrill')
|
|
88
|
+
show_row_headers: Optional[bool] = Field(None, serialization_alias='ShowRowHeaders')
|
|
89
|
+
show_column_headers: Optional[bool] = Field(None, serialization_alias='ShowColHeaders')
|
|
90
|
+
show_row_stripes: Optional[bool] = Field(None, serialization_alias='ShowRowStripes')
|
|
91
|
+
show_col_stripes: Optional[bool] = Field(None, serialization_alias='ShowColStripes')
|
|
92
|
+
show_last_column: Optional[bool] = Field(None, serialization_alias='ShowLastColumn')
|
|
93
|
+
use_auto_formatting: Optional[bool] = Field(None, serialization_alias='UseAutoFormatting')
|
|
94
|
+
page_over_then_down: Optional[bool] = Field(None, serialization_alias='PageOverThenDown')
|
|
95
|
+
merge_item: Optional[bool] = Field(None, serialization_alias='MergeItem')
|
|
96
|
+
compact_data: Optional[bool] = Field(None, serialization_alias='CompactData')
|
|
97
|
+
show_error: Optional[bool] = Field(None, serialization_alias='ShowError')
|
|
98
|
+
classic_layout: Optional[bool] = Field(None, serialization_alias='ClassicLayout')
|
|
99
|
+
pivot_table_style_name: Optional[str] = Field(None, serialization_alias='PivotTableStyleName')
|
|
100
|
+
|
|
101
|
+
@field_validator('data_range')
|
|
102
|
+
@classmethod
|
|
103
|
+
def data_range_validator(cls, data_range: str) -> str:
|
|
104
|
+
if '!' not in data_range or ':' not in data_range:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
'Invalid data range. Expected format: Sheet1!A1:B2 or Sheet1!$A$1:$B$2'
|
|
107
|
+
)
|
|
108
|
+
return data_range
|
|
109
|
+
|
|
110
|
+
@field_validator('pivot_table_range')
|
|
111
|
+
@classmethod
|
|
112
|
+
def pivot_table_range_validator(cls, pivot_table_range: str) -> str:
|
|
113
|
+
if '!' not in pivot_table_range or ':' not in pivot_table_range:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
'Invalid pivot_table_range. Expected format: Sheet1!A1:B2 or Sheet1!$A$1:$B$2'
|
|
116
|
+
)
|
|
117
|
+
return pivot_table_range
|
|
118
|
+
|
|
119
|
+
@field_validator('pivot_table_style_name')
|
|
120
|
+
@classmethod
|
|
121
|
+
def pivot_table_style_name_validator(cls, style: str) -> str:
|
|
122
|
+
if style not in _pivot_table_style:
|
|
123
|
+
raise ValueError(f'Invalid table style name. Expected one of {_pivot_table_style}')
|
|
124
|
+
return style
|
|
Binary file
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, field_serializer, model_serializer
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from ._typing import CommentTextStructure, SetPanesSelection
|
|
7
|
+
from .utils import CommentText, Selection, _validate_cell_reference
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PanesSerializer(BaseModel):
|
|
11
|
+
selection: Optional[SetPanesSelection | list[Selection] | Selection] = None
|
|
12
|
+
|
|
13
|
+
@field_serializer('selection')
|
|
14
|
+
@classmethod
|
|
15
|
+
def serialize_selection(
|
|
16
|
+
cls,
|
|
17
|
+
selection: Optional[SetPanesSelection | list[Selection] | Selection],
|
|
18
|
+
) -> list[dict[str, int]]:
|
|
19
|
+
if selection is None:
|
|
20
|
+
selection = []
|
|
21
|
+
elif not isinstance(selection, list):
|
|
22
|
+
selection = [selection]
|
|
23
|
+
|
|
24
|
+
selection = [item.to_dict() for item in selection if isinstance(item, Selection)]
|
|
25
|
+
|
|
26
|
+
return selection
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommentSerializer(BaseModel):
|
|
30
|
+
text: CommentTextStructure | CommentText | list[CommentText]
|
|
31
|
+
|
|
32
|
+
@field_serializer('text')
|
|
33
|
+
@classmethod
|
|
34
|
+
def serialize_text(
|
|
35
|
+
cls,
|
|
36
|
+
text: CommentTextStructure | CommentText | list[CommentText],
|
|
37
|
+
) -> list[dict[str, str]]:
|
|
38
|
+
text = (
|
|
39
|
+
[text]
|
|
40
|
+
if isinstance(text, (str, CommentText))
|
|
41
|
+
else text
|
|
42
|
+
if isinstance(text, list)
|
|
43
|
+
else [text]
|
|
44
|
+
)
|
|
45
|
+
if all(isinstance(item, (dict, str)) for item in text):
|
|
46
|
+
for idx, item in enumerate(text):
|
|
47
|
+
if isinstance(item, str):
|
|
48
|
+
text[idx] = {'text': item}
|
|
49
|
+
else:
|
|
50
|
+
if 'text' not in item:
|
|
51
|
+
raise ValueError('Comment text should contain the key "text".')
|
|
52
|
+
text[idx] = {
|
|
53
|
+
k[0].upper() + k[1:] if k != 'text' else k: v for k, v in item.items()
|
|
54
|
+
}
|
|
55
|
+
elif all(isinstance(item, CommentText) for item in text):
|
|
56
|
+
text = [t.to_dict() for t in text]
|
|
57
|
+
return text
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DataValidationSerializer(BaseModel):
|
|
61
|
+
set_range: Optional[list[int | float]]
|
|
62
|
+
input_msg: Optional[list[str]]
|
|
63
|
+
drop_list: Optional[list[str | int | float] | str]
|
|
64
|
+
error_msg: Optional[list[str]]
|
|
65
|
+
|
|
66
|
+
@model_serializer(mode='plain')
|
|
67
|
+
def model_serialize(self) -> dict[str, Any]:
|
|
68
|
+
drop_list_key = 'drop_list'
|
|
69
|
+
if isinstance(self.drop_list, str):
|
|
70
|
+
if ':' not in self.drop_list:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
'Invalid drop list. Sequential Reference'
|
|
73
|
+
'Drop list should be in the format "A1:B2".',
|
|
74
|
+
)
|
|
75
|
+
drop_list_split = self.drop_list.split(':')
|
|
76
|
+
_validate_cell_reference(drop_list_split[0])
|
|
77
|
+
_validate_cell_reference(drop_list_split[1])
|
|
78
|
+
drop_list_key = 'sqref_drop_list'
|
|
79
|
+
elif self.drop_list is not None:
|
|
80
|
+
self.drop_list = [str(x) for x in self.drop_list]
|
|
81
|
+
|
|
82
|
+
dv = {}
|
|
83
|
+
if self.set_range is not None:
|
|
84
|
+
if not isinstance(self.set_range, list) or len(self.set_range) != 2:
|
|
85
|
+
raise ValueError('Set range should be a list of two elements. Like [1, 10].')
|
|
86
|
+
dv['set_range'] = self.set_range
|
|
87
|
+
if self.input_msg is not None:
|
|
88
|
+
if not isinstance(self.input_msg, list) or len(self.input_msg) != 2:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
'Input message should be a list of two elements. Like ["Title", "Body"].',
|
|
91
|
+
)
|
|
92
|
+
dv['input_title'] = self.input_msg[0]
|
|
93
|
+
dv['input_body'] = self.input_msg[1]
|
|
94
|
+
if self.drop_list is not None:
|
|
95
|
+
dv[drop_list_key] = self.drop_list
|
|
96
|
+
if self.error_msg is not None:
|
|
97
|
+
dv['error_title'] = self.error_msg[0]
|
|
98
|
+
dv['error_body'] = self.error_msg[1]
|
|
99
|
+
|
|
100
|
+
return dv
|
pyfastexcel/style.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from openpyxl_style_writer import CustomStyle
|
|
8
|
+
from .logformatter import formatter, log_warning
|
|
9
|
+
|
|
10
|
+
BASE_DIR = Path(__file__).resolve().parent
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
style_formatter = logging.StreamHandler()
|
|
14
|
+
style_formatter.setFormatter(formatter)
|
|
15
|
+
|
|
16
|
+
logger.addHandler(style_formatter)
|
|
17
|
+
logger.propagate = False
|
|
18
|
+
|
|
19
|
+
# TODO: Implement a CustomStyle without the dependency of openpyxl_style_writer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StyleManager:
|
|
23
|
+
"""
|
|
24
|
+
A class to set custom styles for Excel files.
|
|
25
|
+
|
|
26
|
+
### Attributes:
|
|
27
|
+
BORDER_TO_INDEX (dict[str, int]): Mapping of border styles to excelize's
|
|
28
|
+
corresponding index.
|
|
29
|
+
|
|
30
|
+
### Methods:
|
|
31
|
+
set_custom_style(cls, name: str, custom_style: CustomStyle): Set custom style
|
|
32
|
+
by register method.
|
|
33
|
+
_create_style(): Creates custom styles for the Excel file.
|
|
34
|
+
_get_style_collections(): Gets collections of custom styles.
|
|
35
|
+
_get_default_style(): Gets the default style.
|
|
36
|
+
_update_style_map(style_name: str, custom_style: CustomStyle): Updates
|
|
37
|
+
the style map.
|
|
38
|
+
_get_font_style(style: CustomStyle): Gets the font style.
|
|
39
|
+
_get_fill_style(style: CustomStyle): Gets the fill style.
|
|
40
|
+
_get_border_style(style: CustomStyle): Gets the border style.
|
|
41
|
+
_get_alignment_style(style: CustomStyle): Gets the alignment style.
|
|
42
|
+
_get_protection_style(style: CustomStyle): Gets the protection style.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
BORDER_TO_INDEX = {
|
|
46
|
+
None: 0,
|
|
47
|
+
'thick': 5,
|
|
48
|
+
'slantDashDot': 13,
|
|
49
|
+
'dotted': 4,
|
|
50
|
+
'hair': 7,
|
|
51
|
+
'dashed': 3,
|
|
52
|
+
'double': 6,
|
|
53
|
+
'mediumDashDotDot': 12,
|
|
54
|
+
'medium': 2,
|
|
55
|
+
'dashDotDot': 11,
|
|
56
|
+
'thin': 1,
|
|
57
|
+
'dashDot': 9,
|
|
58
|
+
'mediumDashed': 8,
|
|
59
|
+
'mediumDashDot': 10,
|
|
60
|
+
}
|
|
61
|
+
# The style retrieved from set_custom_style will be stored in
|
|
62
|
+
# REGISTERED_STYLES temporarily. It will be created after any
|
|
63
|
+
# Writer is initialized and calls the self._create_style() method.
|
|
64
|
+
DEFAULT_STYLE = CustomStyle()
|
|
65
|
+
REGISTERED_STYLES = {'DEFAULT_STYLE': DEFAULT_STYLE}
|
|
66
|
+
_STYLE_NAME_MAP = {}
|
|
67
|
+
_STYLE_ID = 0
|
|
68
|
+
# The shared memory in the parent class that stores every CustomStyle
|
|
69
|
+
# from different Writer classes.
|
|
70
|
+
_style_map = {}
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def set_custom_style(cls, name: str, custom_style: CustomStyle):
|
|
74
|
+
if cls.REGISTERED_STYLES.get(name):
|
|
75
|
+
log_warning(
|
|
76
|
+
logger,
|
|
77
|
+
f'{name} has already existed. Overiding the style settings.',
|
|
78
|
+
)
|
|
79
|
+
cls.REGISTERED_STYLES[name] = custom_style
|
|
80
|
+
cls._STYLE_NAME_MAP[custom_style] = name
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def reset_style_configs(cls):
|
|
84
|
+
cls.REGISTERED_STYLES = {'DEFAULT_STYLE': cls.DEFAULT_STYLE}
|
|
85
|
+
cls._STYLE_NAME_MAP = {}
|
|
86
|
+
cls._STYLE_ID = 0
|
|
87
|
+
cls._style_map = {}
|
|
88
|
+
|
|
89
|
+
def _get_default_style(self) -> dict[str, dict[str, Any] | str]:
|
|
90
|
+
"""
|
|
91
|
+
Gets the default style.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
dict[str, dict[str, Any] | str]: A dictionary containing the
|
|
95
|
+
default style settings.
|
|
96
|
+
"""
|
|
97
|
+
return {
|
|
98
|
+
'Font': {},
|
|
99
|
+
'Fill': {},
|
|
100
|
+
'Border': {},
|
|
101
|
+
'Alignment': {},
|
|
102
|
+
'Protection': {},
|
|
103
|
+
'CustomNumFmt': 'general',
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def _update_style_map(self, style_name: str, custom_style: CustomStyle) -> None:
|
|
107
|
+
if self._style_map.get(style_name):
|
|
108
|
+
log_warning(
|
|
109
|
+
logger,
|
|
110
|
+
f'{style_name} has already existed. Overiding the style settings.',
|
|
111
|
+
)
|
|
112
|
+
self._style_map[style_name] = self._get_default_style()
|
|
113
|
+
self._style_map[style_name]['Font'] = self._get_font_style(custom_style)
|
|
114
|
+
self._style_map[style_name]['Fill'] = self._get_fill_style(custom_style)
|
|
115
|
+
self._style_map[style_name]['Border'] = self._get_border_style(custom_style)
|
|
116
|
+
self._style_map[style_name]['Alignment'] = self._get_alignment_style(custom_style)
|
|
117
|
+
self._style_map[style_name]['Protection'] = self._get_protection_style(custom_style)
|
|
118
|
+
self._style_map[style_name]['CustomNumFmt'] = custom_style.number_format
|
|
119
|
+
|
|
120
|
+
def _get_font_style(self, style: CustomStyle) -> dict[str, str | int | bool | None]:
|
|
121
|
+
font_style_map = {}
|
|
122
|
+
if style.font.name:
|
|
123
|
+
font_style_map['Family'] = style.font.name
|
|
124
|
+
if style.font.sz:
|
|
125
|
+
font_style_map['Size'] = style.font.sz
|
|
126
|
+
if style.font.b:
|
|
127
|
+
font_style_map['Bold'] = style.font.b
|
|
128
|
+
if style.font.i:
|
|
129
|
+
font_style_map['Italic'] = style.font.i
|
|
130
|
+
if style.font.strike:
|
|
131
|
+
font_style_map['Strike'] = style.font.strike
|
|
132
|
+
if style.font.u:
|
|
133
|
+
font_style_map['UnderLine'] = style.font.u
|
|
134
|
+
if style.font.color.rgb:
|
|
135
|
+
font_style_map['Color'] = f'#{style.font.color.rgb[2:]}'
|
|
136
|
+
return font_style_map
|
|
137
|
+
|
|
138
|
+
def _get_fill_style(self, style: CustomStyle) -> dict[str, str]:
|
|
139
|
+
fill_style_map = {}
|
|
140
|
+
if style.fill.fgColor.rgb:
|
|
141
|
+
fill_style_map['Color'] = f'#{style.fill.fgColor.rgb[2:]}'
|
|
142
|
+
fill_style_map['Type'] = 'pattern'
|
|
143
|
+
fill_style_map['Pattern'] = 1
|
|
144
|
+
return fill_style_map
|
|
145
|
+
|
|
146
|
+
def _get_border_style(self, style: CustomStyle) -> dict[str, str]:
|
|
147
|
+
border_style_map = {}
|
|
148
|
+
direction = ['left', 'right', 'top', 'bottom']
|
|
149
|
+
|
|
150
|
+
for d in direction:
|
|
151
|
+
border_style_map[d] = {}
|
|
152
|
+
|
|
153
|
+
if style.border.left.style:
|
|
154
|
+
border_style_map['left']['Style'] = self.BORDER_TO_INDEX[style.border.left.style]
|
|
155
|
+
if style.border.right.style:
|
|
156
|
+
border_style_map['right']['Style'] = self.BORDER_TO_INDEX[style.border.right.style]
|
|
157
|
+
if style.border.top.style:
|
|
158
|
+
border_style_map['top']['Style'] = self.BORDER_TO_INDEX[style.border.top.style]
|
|
159
|
+
if style.border.bottom.style:
|
|
160
|
+
border_style_map['bottom']['Style'] = self.BORDER_TO_INDEX[style.border.bottom.style]
|
|
161
|
+
|
|
162
|
+
if style.border.left.color.rgb:
|
|
163
|
+
border_style_map['left']['Color'] = f'#{style.border.left.color.rgb[2:]}'
|
|
164
|
+
if style.border.right.color.rgb:
|
|
165
|
+
border_style_map['right']['Color'] = f'#{style.border.right.color.rgb[2:]}'
|
|
166
|
+
if style.border.top.color.rgb:
|
|
167
|
+
border_style_map['top']['Color'] = f'#{style.border.top.color.rgb[2:]}'
|
|
168
|
+
if style.border.bottom.color.rgb:
|
|
169
|
+
border_style_map['bottom']['Color'] = f'#{style.border.bottom.color.rgb[2:]}'
|
|
170
|
+
return border_style_map
|
|
171
|
+
|
|
172
|
+
def _get_alignment_style(self, style: CustomStyle) -> dict[str, str]:
|
|
173
|
+
ali_style_map = {}
|
|
174
|
+
|
|
175
|
+
if style.ali.horizontal:
|
|
176
|
+
ali_style_map['Horizontal'] = style.ali.horizontal
|
|
177
|
+
if style.ali.vertical:
|
|
178
|
+
ali_style_map['Vertical'] = style.ali.vertical
|
|
179
|
+
if style.ali.wrapText:
|
|
180
|
+
ali_style_map['WrapText'] = style.ali.wrapText
|
|
181
|
+
if style.ali.shrinkToFit:
|
|
182
|
+
ali_style_map['ShrinkToFit'] = style.ali.shrinkToFit
|
|
183
|
+
if style.ali.indent:
|
|
184
|
+
ali_style_map['Indent'] = style.ali.indent
|
|
185
|
+
if style.ali.readingOrder:
|
|
186
|
+
ali_style_map['ReadingOrder'] = style.ali.readingOrder
|
|
187
|
+
if style.ali.textRotation:
|
|
188
|
+
ali_style_map['TextRotation'] = style.ali.textRotation
|
|
189
|
+
if style.ali.justifyLastLine:
|
|
190
|
+
ali_style_map['JustifyLastLine'] = style.ali.justifyLastLine
|
|
191
|
+
if style.ali.relativeIndent:
|
|
192
|
+
ali_style_map['RelativeIndent'] = style.ali.relativeIndent
|
|
193
|
+
|
|
194
|
+
return ali_style_map
|
|
195
|
+
|
|
196
|
+
def _get_protection_style(self, style: CustomStyle) -> dict[str, str]:
|
|
197
|
+
protection_style_map = {}
|
|
198
|
+
if style.protection.locked:
|
|
199
|
+
protection_style_map['Locked'] = style.protection.locked
|
|
200
|
+
if style.protection.hidden:
|
|
201
|
+
protection_style_map['Hidden'] = style.protection.hidden
|
|
202
|
+
return protection_style_map
|