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/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