pyfastexcel 0.0.7__tar.gz → 0.0.9__tar.gz

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.
Files changed (24) hide show
  1. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/PKG-INFO +15 -4
  2. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/README.md +14 -3
  3. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/pyfastexcel/__init__.py +3 -3
  4. pyfastexcel-0.0.9/pyfastexcel/_typing.py +25 -0
  5. pyfastexcel-0.0.9/pyfastexcel/driver.py +192 -0
  6. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/pyfastexcel/pyfastexcel.dll +0 -0
  7. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/pyfastexcel/pyfastexcel.so +0 -0
  8. pyfastexcel-0.0.9/pyfastexcel/style.py +198 -0
  9. pyfastexcel-0.0.9/pyfastexcel/utils.py +177 -0
  10. pyfastexcel-0.0.9/pyfastexcel/workbook.py +198 -0
  11. pyfastexcel-0.0.9/pyfastexcel/worksheet.py +627 -0
  12. pyfastexcel-0.0.9/pyfastexcel/writer.py +60 -0
  13. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/pyfastexcel.egg-info/PKG-INFO +15 -4
  14. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/pyfastexcel.egg-info/SOURCES.txt +4 -0
  15. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/setup.cfg +1 -1
  16. pyfastexcel-0.0.7/pyfastexcel/driver.py +0 -717
  17. pyfastexcel-0.0.7/pyfastexcel/utils.py +0 -92
  18. pyfastexcel-0.0.7/pyfastexcel/writer.py +0 -263
  19. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/LICENSE +0 -0
  20. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/pyfastexcel.egg-info/dependency_links.txt +0 -0
  21. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/pyfastexcel.egg-info/not-zip-safe +0 -0
  22. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/pyfastexcel.egg-info/requires.txt +0 -0
  23. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/pyfastexcel.egg-info/top_level.txt +0 -0
  24. {pyfastexcel-0.0.7 → pyfastexcel-0.0.9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyfastexcel
3
- Version: 0.0.7
3
+ Version: 0.0.9
4
4
  Summary: High performace excel file generation library.
5
5
  Home-page: https://github.com/Zncl2222/pyfastexcel
6
6
  Author: Zncl2222
@@ -100,7 +100,7 @@ If you prefer to build the package manually, follow these steps:
100
100
 
101
101
  ## Usage
102
102
 
103
- The index assignment is now avaliable in `Workbook` and the `FastWriter`.
103
+ The index assignment is now avaliable in `Workbook` and the `StreamWriter`.
104
104
  Here is the example usage:
105
105
 
106
106
  ```python
@@ -146,8 +146,8 @@ if __name__ == '__main__':
146
146
 
147
147
  ```
148
148
 
149
- You can also using the `FastWriter` or `NormalWriter` which was the
150
- subclass of `Workbook` to write excel row by row, see the more in documentations [quickstart](https://pyfastexcel.readthedocs.io/en/stable/quickstart/).
149
+ You can also using the `StreamWriter` which was the
150
+ subclass of `Workbook` to write excel row by row, see more in [StreamWriter](https://pyfastexcel.readthedocs.io/en/stable/writer/).
151
151
 
152
152
  ## Documentation
153
153
 
@@ -157,6 +157,17 @@ The documentation is hosted on Read the Docs.
157
157
 
158
158
  - [Latest Stable Version](https://pyfastexcel.readthedocs.io/en/stable/)
159
159
 
160
+ ## Benchmark
161
+
162
+ The following result displays the performance comparison between
163
+ `pyfastexcel` and `openpyxl` for writing 50000 rows with 30
164
+ columns (Total 1500000 cells). To see more benchmark results, please
165
+ see the [benchmark](https://pyfastexcel.readthedocs.io/en/stable/benchmark/).
166
+
167
+ <dev align='center'>
168
+ <img src='./docs/images/50000_30_horizontal_Windows11.png' width="80%" height="45%" >
169
+ </dev>
170
+
160
171
  ## How it Works
161
172
 
162
173
  The core functionality revolves around encoding Excel cell data and styles,
@@ -76,7 +76,7 @@ If you prefer to build the package manually, follow these steps:
76
76
 
77
77
  ## Usage
78
78
 
79
- The index assignment is now avaliable in `Workbook` and the `FastWriter`.
79
+ The index assignment is now avaliable in `Workbook` and the `StreamWriter`.
80
80
  Here is the example usage:
81
81
 
82
82
  ```python
@@ -122,8 +122,8 @@ if __name__ == '__main__':
122
122
 
123
123
  ```
124
124
 
125
- You can also using the `FastWriter` or `NormalWriter` which was the
126
- subclass of `Workbook` to write excel row by row, see the more in documentations [quickstart](https://pyfastexcel.readthedocs.io/en/stable/quickstart/).
125
+ You can also using the `StreamWriter` which was the
126
+ subclass of `Workbook` to write excel row by row, see more in [StreamWriter](https://pyfastexcel.readthedocs.io/en/stable/writer/).
127
127
 
128
128
  ## Documentation
129
129
 
@@ -133,6 +133,17 @@ The documentation is hosted on Read the Docs.
133
133
 
134
134
  - [Latest Stable Version](https://pyfastexcel.readthedocs.io/en/stable/)
135
135
 
136
+ ## Benchmark
137
+
138
+ The following result displays the performance comparison between
139
+ `pyfastexcel` and `openpyxl` for writing 50000 rows with 30
140
+ columns (Total 1500000 cells). To see more benchmark results, please
141
+ see the [benchmark](https://pyfastexcel.readthedocs.io/en/stable/benchmark/).
142
+
143
+ <dev align='center'>
144
+ <img src='./docs/images/50000_30_horizontal_Windows11.png' width="80%" height="45%" >
145
+ </dev>
146
+
136
147
  ## How it Works
137
148
 
138
149
  The core functionality revolves around encoding Excel cell data and styles,
@@ -1,11 +1,11 @@
1
1
  from openpyxl_style_writer import CustomStyle, DefaultStyle
2
2
 
3
- from pyfastexcel.writer import FastWriter, NormalWriter, Workbook
3
+ from pyfastexcel.workbook import Workbook
4
+ from pyfastexcel.writer import StreamWriter
4
5
 
5
6
  __all__ = [
6
7
  'Workbook',
7
- 'FastWriter',
8
- 'NormalWriter',
8
+ 'StreamWriter',
9
9
  # Temporary link the CustomStyle from openpyxl_style_writer for
10
10
  # convinent usage.
11
11
  'CustomStyle',
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, TypedDict, Optional, List, Union
4
+
5
+
6
+ class CommentTextDict(TypedDict, total=False):
7
+ text: str
8
+ size: Optional[int]
9
+ name: Optional[str]
10
+ bold: Optional[bool]
11
+ italic: Optional[bool]
12
+ underline: Optional[Literal['single', 'double']]
13
+ strike: Optional[bool]
14
+ vert_align: Optional[str]
15
+ color: Optional[str]
16
+
17
+
18
+ class SelectionDict(TypedDict, total=False):
19
+ sq_ref: str
20
+ active_cell: str
21
+ pane: str
22
+
23
+
24
+ CommentTextStructure = Union[str, List[str], CommentTextDict, List[CommentTextDict]]
25
+ SetPanesSelection = List[SelectionDict]
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import ctypes
5
+ import sys
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ import msgspec
10
+ from openpyxl_style_writer import CustomStyle
11
+
12
+ from .style import StyleManager
13
+ from .worksheet import WorkSheet
14
+
15
+ BASE_DIR = Path(__file__).resolve().parent
16
+
17
+
18
+ class ExcelDriver:
19
+ """
20
+ A driver class to write data to Excel files using custom styles.
21
+
22
+ ### Attributes:
23
+ _FILE_PROPS (dict[str, str]): Default file properties for the Excel
24
+ file.
25
+ _PROTECT_ALGORITHM (tuple[str]): Algorithm for the workbook protection
26
+
27
+ ### Methods:
28
+ __init__(): Initializes the ExcelDriver.
29
+ _read_lib(lib_path: str): Reads a library for Excel manipulation.
30
+ read_lib_and_create_excel(lib_path: str = None): Reads the library and
31
+ creates the Excel file.
32
+ """
33
+
34
+ _FILE_PROPS = {
35
+ 'Category': '',
36
+ 'ContentStatus': '',
37
+ 'Created': '',
38
+ 'Creator': 'pyfastexcel',
39
+ 'Description': '',
40
+ 'Identifier': 'xlsx',
41
+ 'Keywords': 'spreadsheet',
42
+ 'LastModifiedBy': 'pyfastexcel',
43
+ 'Modified': '',
44
+ 'Revision': '0',
45
+ 'Subject': '',
46
+ 'Title': '',
47
+ 'Language': 'en-Us',
48
+ 'Version': '',
49
+ }
50
+ _PROTECT_ALGORITHM = (
51
+ 'XOR',
52
+ 'MD4',
53
+ 'MD5',
54
+ 'SHA-1',
55
+ 'SHA-256',
56
+ 'SHA-384',
57
+ 'SHA-512',
58
+ )
59
+
60
+ def __init__(self, pre_allocate: dict[str, int] = None, plain_data: list[list[str]] = None):
61
+ """
62
+ Initializes the Workbook with default settings and initializes Sheet1.
63
+
64
+ It initializes the workbook structure with Sheet1 as the default sheet,
65
+ sets default file properties, initializes current sheet and sheet list,
66
+ and initializes dictionaries for workbook and protection settings.
67
+
68
+ Args:
69
+ pre_allocate (dict[str, int], optional): A dictionary containing 'n_rows' and 'n_cols'
70
+ keys specifying the dimensions for pre-allocating data in Sheet1.
71
+ plain_data (list[list[str]], optional): A 2D list of strings representing initial data
72
+ to populate Sheet1.
73
+ """
74
+ self.workbook = {
75
+ 'Sheet1': WorkSheet(pre_allocate=pre_allocate, plain_data=plain_data),
76
+ }
77
+ self.file_props = self._get_default_file_props()
78
+ self.sheet = 'Sheet1'
79
+ self._sheet_list = tuple(['Sheet1'])
80
+ self._dict_wb = {}
81
+ self.protection = {}
82
+ self.style = StyleManager()
83
+
84
+ @property
85
+ def sheet_list(self):
86
+ return self._sheet_list
87
+
88
+ def save(self, path: str = './pyfastexcel.xlsx') -> None:
89
+ if not hasattr(self, 'decoded_bytes'):
90
+ self.read_lib_and_create_excel()
91
+
92
+ with open(path, 'wb') as file:
93
+ file.write(self.decoded_bytes)
94
+
95
+ def __getitem__(self, key: str) -> WorkSheet:
96
+ return self.workbook[key]
97
+
98
+ def _check_if_sheet_exists(self, sheet_name: str) -> None:
99
+ if sheet_name not in self.sheet_list:
100
+ raise KeyError(f'{sheet_name} Sheet Does Not Exist.')
101
+
102
+ def read_lib_and_create_excel(self, lib_path: str = None) -> bytes:
103
+ """
104
+ Reads the library and creates the Excel file.
105
+
106
+ Args:
107
+ lib_path (str, optional): The path to the library. Defaults to None.
108
+
109
+ Returns:
110
+ bytes: The byte data of the created Excel file.
111
+ """
112
+ pyfastexcel = self._read_lib(lib_path)
113
+ self._create_style()
114
+
115
+ # Transfer all WorkSheet Object to the sheet dictionary in the workbook.
116
+ for sheet in self._sheet_list:
117
+ self._dict_wb[sheet] = self.workbook[sheet]._transfer_to_dict()
118
+
119
+ results = {
120
+ 'content': self._dict_wb,
121
+ 'file_props': self.file_props,
122
+ 'style': self.style._style_map,
123
+ 'protection': self.protection,
124
+ }
125
+ json_data = msgspec.json.encode(results)
126
+ create_excel = pyfastexcel.Export
127
+ free_pointer = pyfastexcel.FreeCPointer
128
+ free_pointer.argtypes = [ctypes.c_void_p]
129
+ create_excel.argtypes = [ctypes.c_char_p]
130
+ create_excel.restype = ctypes.c_void_p
131
+ byte_data = create_excel(json_data)
132
+ self.decoded_bytes = base64.b64decode(ctypes.cast(byte_data, ctypes.c_char_p).value)
133
+ free_pointer(byte_data)
134
+ StyleManager.reset_style_configs()
135
+ return self.decoded_bytes
136
+
137
+ def _read_lib(self, lib_path: str) -> ctypes.CDLL:
138
+ """
139
+ Reads a shared-library for writing Excel.
140
+
141
+ Args:
142
+ lib_path (str): The path to the library.
143
+
144
+ Returns:
145
+ ctypes.CDLL: The library object.
146
+ """
147
+ if lib_path is None:
148
+ if sys.platform.startswith('linux'):
149
+ lib_path = str(list(BASE_DIR.glob('**/*.so'))[0])
150
+ elif sys.platform.startswith('win32'):
151
+ lib_path = str(list(BASE_DIR.glob('**/*.dll'))[0])
152
+ lib = ctypes.CDLL(lib_path, winmode=0)
153
+ return lib
154
+
155
+ def _get_default_file_props(self) -> dict[str, str]:
156
+ now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
157
+ file_props = self._FILE_PROPS.copy()
158
+ file_props['Created'] = now
159
+ file_props['Modified'] = now
160
+ return file_props
161
+
162
+ def _get_style_collections(self) -> dict[str, CustomStyle]:
163
+ """
164
+ Gets collections of custom styles.
165
+
166
+ Returns:
167
+ dict[str, CustomStyle]: A dictionary containing custom style
168
+ collections.
169
+ """
170
+ return {
171
+ attr: getattr(self, attr)
172
+ for attr in dir(self)
173
+ if not callable(getattr(self, attr)) and isinstance(getattr(self, attr), CustomStyle)
174
+ }
175
+
176
+ def _create_style(self) -> None:
177
+ """
178
+ Creates custom styles for the Excel file.
179
+
180
+ This method initializes custom styles for the Excel file based on
181
+ predefined attributes.
182
+ """
183
+ style_collections = self._get_style_collections()
184
+ self.style._STYLE_NAME_MAP.update({val: key for key, val in style_collections.items()})
185
+
186
+ # Set the CustomStyle from the pre-defined class attributes.
187
+ for key, val in style_collections.items():
188
+ self.style._update_style_map(key, val)
189
+
190
+ # Set the CustomStyle from the REGISTERED method.
191
+ for key, val in self.style.REGISTERED_STYLES.items():
192
+ self.style._update_style_map(key, val)
@@ -0,0 +1,198 @@
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
+
9
+ BASE_DIR = Path(__file__).resolve().parent
10
+
11
+ logger = logging.getLogger(__name__)
12
+ logger.setLevel(logging.DEBUG)
13
+
14
+ ch = logging.StreamHandler()
15
+ ch.setLevel(logging.DEBUG)
16
+
17
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
18
+ ch.setFormatter(formatter)
19
+
20
+ if not logger.hasHandlers():
21
+ logger.addHandler(ch)
22
+
23
+ # TODO: Implement a CustomStyle without the dependency of openpyxl_style_writer
24
+
25
+
26
+ class StyleManager:
27
+ """
28
+ A class to set custom styles for Excel files.
29
+
30
+ ### Attributes:
31
+ BORDER_TO_INDEX (dict[str, int]): Mapping of border styles to excelize's
32
+ corresponding index.
33
+
34
+ ### Methods:
35
+ set_custom_style(cls, name: str, custom_style: CustomStyle): Set custom style
36
+ by register method.
37
+ _create_style(): Creates custom styles for the Excel file.
38
+ _get_style_collections(): Gets collections of custom styles.
39
+ _get_default_style(): Gets the default style.
40
+ _update_style_map(style_name: str, custom_style: CustomStyle): Updates
41
+ the style map.
42
+ _get_font_style(style: CustomStyle): Gets the font style.
43
+ _get_fill_style(style: CustomStyle): Gets the fill style.
44
+ _get_border_style(style: CustomStyle): Gets the border style.
45
+ _get_alignment_style(style: CustomStyle): Gets the alignment style.
46
+ _get_protection_style(style: CustomStyle): Gets the protection style.
47
+ """
48
+
49
+ BORDER_TO_INDEX = {
50
+ None: 0,
51
+ 'thick': 5,
52
+ 'slantDashDot': 13,
53
+ 'dotted': 4,
54
+ 'hair': 7,
55
+ 'dashed': 3,
56
+ 'double': 6,
57
+ 'mediumDashDotDot': 12,
58
+ 'medium': 2,
59
+ 'dashDotDot': 11,
60
+ 'thin': 1,
61
+ 'dashDot': 9,
62
+ 'mediumDashed': 8,
63
+ 'mediumDashDot': 10,
64
+ }
65
+ # The style retrieved from set_custom_style will be stored in
66
+ # REGISTERED_STYLES temporarily. It will be created after any
67
+ # Writer is initialized and calls the self._create_style() method.
68
+ DEFAULT_STYLE = CustomStyle()
69
+ REGISTERED_STYLES = {'DEFAULT_STYLE': DEFAULT_STYLE}
70
+ _STYLE_NAME_MAP = {}
71
+ _STYLE_ID = 0
72
+ # The shared memory in the parent class that stores every CustomStyle
73
+ # from different Writer classes.
74
+ _style_map = {}
75
+
76
+ @classmethod
77
+ def set_custom_style(cls, name: str, custom_style: CustomStyle):
78
+ cls.REGISTERED_STYLES[name] = custom_style
79
+ cls._STYLE_NAME_MAP[custom_style] = name
80
+
81
+ @classmethod
82
+ def reset_style_configs(cls):
83
+ cls.REGISTERED_STYLES = {'DEFAULT_STYLE': cls.DEFAULT_STYLE}
84
+ cls._STYLE_NAME_MAP = {}
85
+ cls._STYLE_ID = 0
86
+ cls._style_map = {}
87
+
88
+ def _get_default_style(self) -> dict[str, dict[str, Any] | str]:
89
+ """
90
+ Gets the default style.
91
+
92
+ Returns:
93
+ dict[str, dict[str, Any] | str]: A dictionary containing the
94
+ default style settings.
95
+ """
96
+ return {
97
+ 'Font': {},
98
+ 'Fill': {},
99
+ 'Border': {},
100
+ 'Alignment': {},
101
+ 'Protection': {},
102
+ 'CustomNumFmt': 'general',
103
+ }
104
+
105
+ def _update_style_map(self, style_name: str, custom_style: CustomStyle) -> None:
106
+ if self._style_map.get(style_name):
107
+ logger.warning(f'{style_name} has already existed. Overiding the style settings.')
108
+ self._style_map[style_name] = self._get_default_style()
109
+ self._style_map[style_name]['Font'] = self._get_font_style(custom_style)
110
+ self._style_map[style_name]['Fill'] = self._get_fill_style(custom_style)
111
+ self._style_map[style_name]['Border'] = self._get_border_style(custom_style)
112
+ self._style_map[style_name]['Alignment'] = self._get_alignment_style(custom_style)
113
+ self._style_map[style_name]['Protection'] = self._get_protection_style(custom_style)
114
+ self._style_map[style_name]['CustomNumFmt'] = custom_style.number_format
115
+
116
+ def _get_font_style(self, style: CustomStyle) -> dict[str, str | int | bool | None]:
117
+ font_style_map = {}
118
+ if style.font.name:
119
+ font_style_map['Family'] = style.font.name
120
+ if style.font.sz:
121
+ font_style_map['Size'] = style.font.sz
122
+ if style.font.b:
123
+ font_style_map['Bold'] = style.font.b
124
+ if style.font.i:
125
+ font_style_map['Italic'] = style.font.i
126
+ if style.font.strike:
127
+ font_style_map['Strike'] = style.font.strike
128
+ if style.font.u:
129
+ font_style_map['UnderLine'] = style.font.u
130
+ if style.font.color.rgb:
131
+ font_style_map['Color'] = f'#{style.font.color.rgb[2:]}'
132
+ return font_style_map
133
+
134
+ def _get_fill_style(self, style: CustomStyle) -> dict[str, str]:
135
+ fill_style_map = {}
136
+ if style.fill.fgColor.rgb:
137
+ fill_style_map['Color'] = f'#{style.fill.fgColor.rgb[2:]}'
138
+ fill_style_map['Type'] = 'pattern'
139
+ fill_style_map['Pattern'] = 1
140
+ return fill_style_map
141
+
142
+ def _get_border_style(self, style: CustomStyle) -> dict[str, str]:
143
+ border_style_map = {}
144
+ direction = ['left', 'right', 'top', 'bottom']
145
+
146
+ for d in direction:
147
+ border_style_map[d] = {}
148
+
149
+ if style.border.left.style:
150
+ border_style_map['left']['Style'] = self.BORDER_TO_INDEX[style.border.left.style]
151
+ if style.border.right.style:
152
+ border_style_map['right']['Style'] = self.BORDER_TO_INDEX[style.border.right.style]
153
+ if style.border.top.style:
154
+ border_style_map['top']['Style'] = self.BORDER_TO_INDEX[style.border.top.style]
155
+ if style.border.bottom.style:
156
+ border_style_map['bottom']['Style'] = self.BORDER_TO_INDEX[style.border.bottom.style]
157
+
158
+ if style.border.left.color.rgb:
159
+ border_style_map['left']['Color'] = f'#{style.border.left.color.rgb[2:]}'
160
+ if style.border.right.color.rgb:
161
+ border_style_map['right']['Color'] = f'#{style.border.right.color.rgb[2:]}'
162
+ if style.border.top.color.rgb:
163
+ border_style_map['top']['Color'] = f'#{style.border.top.color.rgb[2:]}'
164
+ if style.border.bottom.color.rgb:
165
+ border_style_map['bottom']['Color'] = f'#{style.border.bottom.color.rgb[2:]}'
166
+ return border_style_map
167
+
168
+ def _get_alignment_style(self, style: CustomStyle) -> dict[str, str]:
169
+ ali_style_map = {}
170
+
171
+ if style.ali.horizontal:
172
+ ali_style_map['Horizontal'] = style.ali.horizontal
173
+ if style.ali.vertical:
174
+ ali_style_map['Vertical'] = style.ali.vertical
175
+ if style.ali.wrapText:
176
+ ali_style_map['WrapText'] = style.ali.wrapText
177
+ if style.ali.shrinkToFit:
178
+ ali_style_map['ShrinkToFit'] = style.ali.shrinkToFit
179
+ if style.ali.indent:
180
+ ali_style_map['Indent'] = style.ali.indent
181
+ if style.ali.readingOrder:
182
+ ali_style_map['ReadingOrder'] = style.ali.readingOrder
183
+ if style.ali.textRotation:
184
+ ali_style_map['TextRotation'] = style.ali.textRotation
185
+ if style.ali.justifyLastLine:
186
+ ali_style_map['JustifyLastLine'] = style.ali.justifyLastLine
187
+ if style.ali.relativeIndent:
188
+ ali_style_map['RelativeIndent'] = style.ali.relativeIndent
189
+
190
+ return ali_style_map
191
+
192
+ def _get_protection_style(self, style: CustomStyle) -> dict[str, str]:
193
+ protection_style_map = {}
194
+ if style.protection.locked:
195
+ protection_style_map['Locked'] = style.protection.locked
196
+ if style.protection.hidden:
197
+ protection_style_map['Hidden'] = style.protection.hidden
198
+ return protection_style_map