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.
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/PKG-INFO +12 -1
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/README.md +11 -0
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/pyfastexcel/__init__.py +3 -3
- pyfastexcel-0.0.8/pyfastexcel/driver.py +186 -0
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/pyfastexcel/pyfastexcel.dll +0 -0
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/pyfastexcel/pyfastexcel.so +0 -0
- pyfastexcel-0.0.8/pyfastexcel/style.py +198 -0
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/pyfastexcel/utils.py +34 -11
- pyfastexcel-0.0.8/pyfastexcel/workbook.py +124 -0
- pyfastexcel-0.0.8/pyfastexcel/worksheet.py +407 -0
- pyfastexcel-0.0.8/pyfastexcel/writer.py +60 -0
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/pyfastexcel.egg-info/PKG-INFO +12 -1
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/pyfastexcel.egg-info/SOURCES.txt +3 -0
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/setup.cfg +1 -1
- pyfastexcel-0.0.7/pyfastexcel/driver.py +0 -717
- pyfastexcel-0.0.7/pyfastexcel/writer.py +0 -263
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/LICENSE +0 -0
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/pyfastexcel.egg-info/dependency_links.txt +0 -0
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/pyfastexcel.egg-info/not-zip-safe +0 -0
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/pyfastexcel.egg-info/requires.txt +0 -0
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/pyfastexcel.egg-info/top_level.txt +0 -0
- {pyfastexcel-0.0.7 → pyfastexcel-0.0.8}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyfastexcel
|
|
3
|
-
Version: 0.0.
|
|
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.
|
|
3
|
+
from pyfastexcel.workbook import Workbook
|
|
4
|
+
from pyfastexcel.writer import StreamWriter
|
|
4
5
|
|
|
5
6
|
__all__ = [
|
|
6
7
|
'Workbook',
|
|
7
|
-
'
|
|
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)
|
|
Binary file
|
|
Binary file
|
|
@@ -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 .
|
|
22
|
+
from .style import StyleManager
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
StyleManager.set_custom_style(style_name, style)
|
|
14
25
|
|
|
15
26
|
|
|
16
27
|
def validate_and_register_style(style: CustomStyle) -> None:
|
|
17
|
-
from .
|
|
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 {
|
|
24
|
-
|
|
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
|
|
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 =
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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)
|