pyfastexcel 0.0.7__tar.gz → 0.0.8__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyfastexcel
3
- Version: 0.0.7
3
+ Version: 0.0.8
4
4
  Summary: High performace excel file generation library.
5
5
  Home-page: https://github.com/Zncl2222/pyfastexcel
6
6
  Author: Zncl2222
@@ -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,
@@ -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,186 @@
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
+ BORDER_TO_INDEX (dict[str, int]): Mapping of border styles to excelize's
24
+ corresponding index.
25
+ _FILE_PROPS (dict[str, str]): Default file properties for the Excel
26
+ file.
27
+
28
+ ### Methods:
29
+ __init__(): Initializes the ExcelDriver.
30
+ _read_lib(lib_path: str): Reads a library for Excel manipulation.
31
+ read_lib_and_create_excel(lib_path: str = None): Reads the library and
32
+ creates the Excel file.
33
+ """
34
+
35
+ _FILE_PROPS = {
36
+ 'Category': '',
37
+ 'ContentStatus': '',
38
+ 'Created': '',
39
+ 'Creator': 'pyfastexcel',
40
+ 'Description': '',
41
+ 'Identifier': 'xlsx',
42
+ 'Keywords': 'spreadsheet',
43
+ 'LastModifiedBy': 'pyfastexcel',
44
+ 'Modified': '',
45
+ 'Revision': '0',
46
+ 'Subject': '',
47
+ 'Title': '',
48
+ 'Language': 'en-Us',
49
+ 'Version': '',
50
+ }
51
+ _PROTECT_ALGORITHM = (
52
+ 'XOR',
53
+ 'MD4',
54
+ 'MD5',
55
+ 'SHA-1',
56
+ 'SHA-256',
57
+ 'SHA-384',
58
+ 'SHA-512',
59
+ )
60
+
61
+ def __init__(self):
62
+ """
63
+ Initializes the PyExcelizeDriver.
64
+
65
+ It initializes the Excel data, file properties, default sheet,
66
+ current sheet, and style mappings.
67
+ """
68
+ self.workbook = {
69
+ 'Sheet1': WorkSheet(),
70
+ }
71
+ self.file_props = self._get_default_file_props()
72
+ self.sheet = 'Sheet1'
73
+ self._sheet_list = tuple(['Sheet1'])
74
+ self._dict_wb = {}
75
+ self.protection = {}
76
+ self.style = StyleManager()
77
+
78
+ @property
79
+ def sheet_list(self):
80
+ return self._sheet_list
81
+
82
+ def save(self, path: str = './pyfastexcel.xlsx') -> None:
83
+ if not hasattr(self, 'decoded_bytes'):
84
+ self.read_lib_and_create_excel()
85
+
86
+ with open(path, 'wb') as file:
87
+ file.write(self.decoded_bytes)
88
+
89
+ def __getitem__(self, key: str) -> WorkSheet:
90
+ return self.workbook[key]
91
+
92
+ def _check_if_sheet_exists(self, sheet_name: str) -> None:
93
+ if sheet_name not in self.sheet_list:
94
+ raise KeyError(f'{sheet_name} Sheet Does Not Exist.')
95
+
96
+ def read_lib_and_create_excel(self, lib_path: str = None) -> bytes:
97
+ """
98
+ Reads the library and creates the Excel file.
99
+
100
+ Args:
101
+ lib_path (str, optional): The path to the library. Defaults to None.
102
+
103
+ Returns:
104
+ bytes: The byte data of the created Excel file.
105
+ """
106
+ pyfastexcel = self._read_lib(lib_path)
107
+ self._create_style()
108
+
109
+ # Transfer all WorkSheet Object to the sheet dictionary in the workbook.
110
+ for sheet in self._sheet_list:
111
+ self._dict_wb[sheet] = self.workbook[sheet]._transfer_to_dict()
112
+
113
+ results = {
114
+ 'content': self._dict_wb,
115
+ 'file_props': self.file_props,
116
+ 'style': self.style._style_map,
117
+ 'protection': self.protection,
118
+ }
119
+ json_data = msgspec.json.encode(results)
120
+ create_excel = pyfastexcel.Export
121
+ free_pointer = pyfastexcel.FreeCPointer
122
+ free_pointer.argtypes = [ctypes.c_void_p]
123
+ create_excel.argtypes = [ctypes.c_char_p]
124
+ create_excel.restype = ctypes.c_void_p
125
+ byte_data = create_excel(json_data)
126
+ self.decoded_bytes = base64.b64decode(ctypes.cast(byte_data, ctypes.c_char_p).value)
127
+ free_pointer(byte_data)
128
+ StyleManager.reset_style_configs()
129
+ return self.decoded_bytes
130
+
131
+ def _read_lib(self, lib_path: str) -> ctypes.CDLL:
132
+ """
133
+ Reads a shared-library for writing Excel.
134
+
135
+ Args:
136
+ lib_path (str): The path to the library.
137
+
138
+ Returns:
139
+ ctypes.CDLL: The library object.
140
+ """
141
+ if lib_path is None:
142
+ if sys.platform.startswith('linux'):
143
+ lib_path = str(list(BASE_DIR.glob('**/*.so'))[0])
144
+ elif sys.platform.startswith('win32'):
145
+ lib_path = str(list(BASE_DIR.glob('**/*.dll'))[0])
146
+ lib = ctypes.CDLL(lib_path, winmode=0)
147
+ return lib
148
+
149
+ def _get_default_file_props(self) -> dict[str, str]:
150
+ now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
151
+ file_props = self._FILE_PROPS.copy()
152
+ file_props['Created'] = now
153
+ file_props['Modified'] = now
154
+ return file_props
155
+
156
+ def _get_style_collections(self) -> dict[str, CustomStyle]:
157
+ """
158
+ Gets collections of custom styles.
159
+
160
+ Returns:
161
+ dict[str, CustomStyle]: A dictionary containing custom style
162
+ collections.
163
+ """
164
+ return {
165
+ attr: getattr(self, attr)
166
+ for attr in dir(self)
167
+ if not callable(getattr(self, attr)) and isinstance(getattr(self, attr), CustomStyle)
168
+ }
169
+
170
+ def _create_style(self) -> None:
171
+ """
172
+ Creates custom styles for the Excel file.
173
+
174
+ This method initializes custom styles for the Excel file based on
175
+ predefined attributes.
176
+ """
177
+ style_collections = self._get_style_collections()
178
+ self.style._STYLE_NAME_MAP.update({val: key for key, val in style_collections.items()})
179
+
180
+ # Set the CustomStyle from the pre-defined class attributes.
181
+ for key, val in style_collections.items():
182
+ self.style._update_style_map(key, val)
183
+
184
+ # Set the CustomStyle from the REGISTERED method.
185
+ for key, val in self.style.REGISTERED_STYLES.items():
186
+ 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
@@ -2,26 +2,37 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  import string
5
+ import warnings
5
6
  from typing import Any, Literal
6
7
 
7
8
  from openpyxl_style_writer import CustomStyle
8
9
 
10
+ warnings.simplefilter('always', DeprecationWarning)
11
+
12
+
13
+ def deprecated_warning(msg: str):
14
+ warnings.warn(
15
+ msg,
16
+ DeprecationWarning,
17
+ stacklevel=2,
18
+ )
19
+
9
20
 
10
21
  def set_custom_style(style_name: str, style: CustomStyle) -> None:
11
- from .driver import ExcelDriver
22
+ from .style import StyleManager
12
23
 
13
- ExcelDriver.set_custom_style(style_name, style)
24
+ StyleManager.set_custom_style(style_name, style)
14
25
 
15
26
 
16
27
  def validate_and_register_style(style: CustomStyle) -> None:
17
- from .driver import ExcelDriver
28
+ from .style import StyleManager
18
29
 
19
30
  if not isinstance(style, CustomStyle):
20
31
  raise TypeError(
21
32
  f'Invalid type ({type(style)}). Style should be a CustomStyle object.',
22
33
  )
23
- set_custom_style(f'Custom Style {ExcelDriver._STYLE_ID}', style)
24
- ExcelDriver._STYLE_ID += 1
34
+ set_custom_style(f'Custom Style {StyleManager._STYLE_ID}', style)
35
+ StyleManager._STYLE_ID += 1
25
36
 
26
37
 
27
38
  def validate_and_format_value(
@@ -37,9 +48,15 @@ def validate_and_format_value(
37
48
  return (value, 'DEFAULT_STYLE') if set_default_style else value
38
49
 
39
50
 
40
- def separate_alpha_numeric(input_string: str):
51
+ def _separate_alpha_numeric(input_string: str) -> tuple[str, str]:
52
+ '''
53
+ Separate the alpha and numeric part of a string.
54
+ Return alpha_part at first index and num_part at second index.
55
+ '''
41
56
  alpha_part = re.findall(r'[a-zA-Z]+', input_string)
42
57
  num_part = re.findall(r'[0-9]+', input_string)
58
+ if len(alpha_part) == 0 or len(num_part) == 0:
59
+ raise ValueError(f'Invalid input string {input_string}.')
43
60
  return alpha_part[0], num_part[0]
44
61
 
45
62
 
@@ -80,13 +97,19 @@ def index_to_column(index: int) -> str:
80
97
 
81
98
 
82
99
  def excel_index_to_list_index(index: str) -> tuple[int, int]:
83
- alpha, num = separate_alpha_numeric(index)
100
+ alpha, num = _separate_alpha_numeric(index)
84
101
  column = column_to_index(alpha)
85
102
  row = int(num)
86
103
  return row - 1, column - 1
87
104
 
88
105
 
89
- def extract_numeric_part(cell_location: str) -> str | None:
90
- numeric_part = re.search(r'\d+', cell_location)
91
- if numeric_part:
92
- return numeric_part.group()
106
+ def _validate_excel_index(index: str) -> bool:
107
+ alpha, num = _separate_alpha_numeric(index)
108
+ num = int(num)
109
+ if not all(c in string.ascii_uppercase for c in alpha):
110
+ raise ValueError(f'Invalid column ({alpha}). Column should be in uppercase.')
111
+ if not _is_valid_column(alpha):
112
+ raise ValueError(f"Invalid column ({alpha}). Maximum Column is 'XFD'.")
113
+ if num < 1 or num > 16384:
114
+ raise ValueError(f'Invalid index ({num}). Index should less and equal to 16384.')
115
+ return True
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ from pyfastexcel.driver import ExcelDriver, WorkSheet
4
+ from pyfastexcel.utils import deprecated_warning
5
+
6
+
7
+ class Workbook(ExcelDriver):
8
+ """
9
+ A base class for writing data to Excel files with custom styles.
10
+
11
+ This class provides methods to set file properties, cell dimensions,
12
+ merge cells, manipulate sheets, and more.
13
+
14
+ Methods:
15
+ remove_sheet(sheet: str) -> None:
16
+ Removes a sheet from the Excel data.
17
+ create_sheet(sheet_name: str) -> None:
18
+ Creates a new sheet.
19
+ switch_sheet(sheet_name: str) -> None:
20
+ Set current self.sheet to a different sheet.
21
+ set_file_props(key: str, value: str) -> None:
22
+ Sets a file property.
23
+ set_cell_width(sheet: str, col: str | int, value: int) -> None:
24
+ Sets the width of a cell.
25
+ set_cell_height(sheet: str, row: int, value: int) -> None:
26
+ Sets the height of a cell.
27
+ set_merge_cell(sheet: str, top_left_cell: str, bottom_right_cell: str) -> None:
28
+ Sets a merge cell range in the specified sheet.
29
+ """
30
+
31
+ def remove_sheet(self, sheet: str) -> None:
32
+ """
33
+ Removes a sheet from the Excel data.
34
+
35
+ Args:
36
+ sheet (str): The name of the sheet to remove.
37
+ """
38
+ if len(self.workbook) == 1:
39
+ raise ValueError('Cannot remove the only sheet in the workbook.')
40
+ if self.workbook.get(sheet) is None:
41
+ raise IndexError(f'Sheet {sheet} does not exist.')
42
+ self.workbook.pop(sheet)
43
+ self._sheet_list = tuple(self.workbook.keys())
44
+ self.sheet = self._sheet_list[0]
45
+
46
+ def create_sheet(self, sheet_name: str) -> None:
47
+ """
48
+ Creates a new sheet, and set it as current self.sheet.
49
+
50
+ Args:
51
+ sheet_name (str): The name of the new sheet.
52
+ """
53
+ if self.workbook.get(sheet_name) is not None:
54
+ raise ValueError(f'Sheet {sheet_name} already exists.')
55
+ self.workbook[sheet_name] = WorkSheet()
56
+ self.sheet = sheet_name
57
+ self._sheet_list = tuple([x for x in self._sheet_list] + [sheet_name])
58
+
59
+ def switch_sheet(self, sheet_name: str) -> None:
60
+ """
61
+ Set current self.sheet to a different sheet. If sheet does not existed
62
+ then raise error.
63
+
64
+ Args:
65
+ sheet_name (str): The name of the sheet to switch to.
66
+
67
+ Raises:
68
+ IndexError: If sheet does not exist.
69
+ """
70
+ self._check_if_sheet_exists(sheet_name)
71
+ self.sheet = sheet_name
72
+
73
+ def set_file_props(self, key: str, value: str) -> None:
74
+ """
75
+ Sets a file property.
76
+
77
+ Args:
78
+ key (str): The property key.
79
+ value (str): The property value.
80
+
81
+ Raises:
82
+ ValueError: If the key is invalid.
83
+ """
84
+ if key not in self._FILE_PROPS:
85
+ raise ValueError(f'Invalid file property: {key}')
86
+ self.file_props[key] = value
87
+
88
+ def protect_workbook(
89
+ self,
90
+ algorithm: str,
91
+ password: str,
92
+ lock_structure: bool = False,
93
+ lock_windows: bool = False,
94
+ ):
95
+ if algorithm not in self._PROTECT_ALGORITHM:
96
+ raise ValueError(
97
+ f'Invalid algorithm, the options are {self._PROTECT_ALGORITHM}',
98
+ )
99
+ self.protection['algorithm'] = algorithm
100
+ self.protection['password'] = password
101
+ self.protection['lock_structure'] = lock_structure
102
+ self.protection['lock_windows'] = lock_windows
103
+
104
+ def set_cell_width(self, sheet: str, col: str | int, value: int) -> None:
105
+ self._check_if_sheet_exists(sheet)
106
+ self.workbook[sheet].set_cell_width(col, value)
107
+
108
+ def set_cell_height(self, sheet: str, row: int, value: int) -> None:
109
+ self._check_if_sheet_exists(sheet)
110
+ self.workbook[sheet].set_cell_height(row, value)
111
+
112
+ def set_merge_cell(self, sheet: str, top_left_cell: str, bottom_right_cell: str) -> None:
113
+ deprecated_warning(
114
+ "This function is going to deprecated in v1.0.0. Please use 'wb.merge_cell' instead",
115
+ )
116
+ self.merge_cell(sheet, top_left_cell, bottom_right_cell)
117
+
118
+ def merge_cell(self, sheet: str, top_left_cell: str, bottom_right_cell: str) -> None:
119
+ self._check_if_sheet_exists(sheet)
120
+ self.workbook[sheet].set_merge_cell(top_left_cell, bottom_right_cell)
121
+
122
+ def auto_filter(self, sheet: str, target_range: str) -> None:
123
+ self._check_if_sheet_exists(sheet)
124
+ self.workbook[sheet].auto_filter(target_range)