pyfastexcel 0.1.2__tar.gz → 0.1.4__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.1.2 → pyfastexcel-0.1.4}/PKG-INFO +3 -1
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/_typing.py +3 -1
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/driver.py +6 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/pyfastexcel.dll +0 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/pyfastexcel.so +0 -0
- pyfastexcel-0.1.4/pyfastexcel/serializers.py +100 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/utils.py +3 -1
- pyfastexcel-0.1.4/pyfastexcel/validators.py +196 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/workbook.py +38 -7
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/worksheet.py +77 -99
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/writer.py +3 -5
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel.egg-info/PKG-INFO +3 -1
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel.egg-info/SOURCES.txt +2 -0
- pyfastexcel-0.1.4/pyfastexcel.egg-info/requires.txt +6 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/setup.cfg +3 -1
- pyfastexcel-0.1.2/pyfastexcel.egg-info/requires.txt +0 -2
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/LICENSE +0 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/README.md +0 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/__init__.py +0 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/logformatter.py +0 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/style.py +0 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel.egg-info/dependency_links.txt +0 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel.egg-info/not-zip-safe +0 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel.egg-info/top_level.txt +0 -0
- {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyfastexcel
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: High performace excel file generation library.
|
|
5
5
|
Home-page: https://github.com/Zncl2222/pyfastexcel
|
|
6
6
|
Author: Zncl2222
|
|
@@ -21,6 +21,8 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: msgspec
|
|
23
23
|
Requires-Dist: openpyxl-style-writer>=1.1.3
|
|
24
|
+
Requires-Dist: pydantic
|
|
25
|
+
Requires-Dist: eval-type-backport; python_version < "3.9"
|
|
24
26
|
|
|
25
27
|
# pyfastexcel
|
|
26
28
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Literal, Protocol,
|
|
3
|
+
from typing import Literal, Protocol, Optional, List, Union, TypeVar
|
|
4
|
+
from typing_extensions import TypedDict
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class CommentTextDict(TypedDict, total=False):
|
|
@@ -25,5 +26,6 @@ class Writable(Protocol):
|
|
|
25
26
|
def write(self, content: str) -> None: ...
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
Self = TypeVar('Self')
|
|
28
30
|
CommentTextStructure = Union[str, List[str], CommentTextDict, List[CommentTextDict]]
|
|
29
31
|
SetPanesSelection = List[SelectionDict]
|
|
@@ -16,6 +16,7 @@ from openpyxl_style_writer import CustomStyle
|
|
|
16
16
|
from .logformatter import formatter
|
|
17
17
|
from .style import StyleManager
|
|
18
18
|
from .worksheet import WorkSheet
|
|
19
|
+
from .validators import TableFinalValidation
|
|
19
20
|
from ._typing import Writable
|
|
20
21
|
|
|
21
22
|
BASE_DIR = Path(__file__).resolve().parent
|
|
@@ -160,6 +161,11 @@ class ExcelDriver:
|
|
|
160
161
|
set_group_columns = True
|
|
161
162
|
if len(self.workbook[sheet]._grouped_rows_list) != 0:
|
|
162
163
|
set_row_columns = True
|
|
164
|
+
if len(self.workbook[sheet]._table_list) != 0:
|
|
165
|
+
TableFinalValidation(
|
|
166
|
+
data=self.workbook[sheet]._data,
|
|
167
|
+
table_list=self.workbook[sheet]._table_list,
|
|
168
|
+
)
|
|
163
169
|
|
|
164
170
|
# Set writer (if some of the function that excelize StremWriter is not support, and
|
|
165
171
|
# system will use normal writer to write the excel)
|
|
Binary file
|
|
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
|
|
@@ -4,7 +4,9 @@ import logging
|
|
|
4
4
|
import re
|
|
5
5
|
import string
|
|
6
6
|
import warnings
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
# from dataclasses import dataclass
|
|
9
|
+
from pydantic.dataclasses import dataclass
|
|
8
10
|
from typing import Any, Literal
|
|
9
11
|
|
|
10
12
|
from openpyxl_style_writer import CustomStyle
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
6
|
+
from typing import Any, Optional, Literal
|
|
7
|
+
|
|
8
|
+
from ._typing import CommentTextStructure, SetPanesSelection, Self
|
|
9
|
+
from .logformatter import formatter
|
|
10
|
+
from .utils import CommentText, Selection, cell_reference_to_index, _validate_cell_reference
|
|
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
|
+
_table_style_config: dict[str, int] = {
|
|
20
|
+
'TableStyleLight': 21,
|
|
21
|
+
'TableStyleMedium': 28,
|
|
22
|
+
'TableStyleDark': 11,
|
|
23
|
+
}
|
|
24
|
+
_table_style: set[str] = set()
|
|
25
|
+
_table_style.add('')
|
|
26
|
+
|
|
27
|
+
for s, num in _table_style_config.items():
|
|
28
|
+
for i in range(1, num + 1):
|
|
29
|
+
_table_style.add(f'{s}{i}')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TableValidator(BaseModel):
|
|
33
|
+
cell_range: str
|
|
34
|
+
name: str
|
|
35
|
+
style_name: str = Field(default='', strict=True)
|
|
36
|
+
show_first_column: bool = Field(default=True, strict=True)
|
|
37
|
+
show_last_column: bool = Field(default=True, strict=True)
|
|
38
|
+
show_row_stripes: bool = Field(default=False, strict=True)
|
|
39
|
+
show_column_stripes: bool = Field(default=True, strict=True)
|
|
40
|
+
validate_table: bool = Field(default=True, strict=True)
|
|
41
|
+
|
|
42
|
+
@field_validator('cell_range')
|
|
43
|
+
@classmethod
|
|
44
|
+
def validate_cell_range(cls, cell_range: str) -> str:
|
|
45
|
+
if ':' not in cell_range:
|
|
46
|
+
raise ValueError('Invalid cell range. Expected format: A1:B2')
|
|
47
|
+
cell_range_split = cell_range.split(':')
|
|
48
|
+
if len(cell_range_split) != 2:
|
|
49
|
+
raise ValueError('Invalid cell range. Expected format: A1:B2')
|
|
50
|
+
|
|
51
|
+
_validate_cell_reference(cell_range_split[0])
|
|
52
|
+
_validate_cell_reference(cell_range_split[1])
|
|
53
|
+
|
|
54
|
+
return cell_range
|
|
55
|
+
|
|
56
|
+
@field_validator('style_name')
|
|
57
|
+
@classmethod
|
|
58
|
+
def validate_style_name(cls, style_name: str) -> str:
|
|
59
|
+
if style_name not in _table_style:
|
|
60
|
+
raise ValueError(f'Invalid table style name. Expected one of {_table_style}')
|
|
61
|
+
return style_name
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TableFinalValidation(BaseModel):
|
|
65
|
+
data: list
|
|
66
|
+
table_list: list[dict[str, Any]]
|
|
67
|
+
|
|
68
|
+
@model_validator(mode='after')
|
|
69
|
+
def validate_table_list(self) -> Self:
|
|
70
|
+
for t in self.table_list:
|
|
71
|
+
if t['validate_table'] is False:
|
|
72
|
+
continue
|
|
73
|
+
t_split = t['range'].split(':')
|
|
74
|
+
start_row, start_col = cell_reference_to_index(t_split[0])
|
|
75
|
+
end_row, end_col = cell_reference_to_index(t_split[1])
|
|
76
|
+
|
|
77
|
+
# Check if table range is valid, end_col should +1 because of length comparison
|
|
78
|
+
if end_col + 1 > len(self.data[start_row]):
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Invalid table range for {t['name']}. "
|
|
81
|
+
'Please write a row for table first row.'
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if len(set(self.data[start_row])) != len(self.data[start_row]):
|
|
85
|
+
raise ValueError(
|
|
86
|
+
'Invalid table header. ' 'The first row contains duplicate values.'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
end_row = len(self.data) - 1 if end_row >= len(self.data) else end_row
|
|
90
|
+
for col in range(start_col, end_col + 1):
|
|
91
|
+
column_data = [
|
|
92
|
+
self.data[row][col] for row in range(start_row, end_row + 1) if self.data[row]
|
|
93
|
+
]
|
|
94
|
+
first_element = column_data[0]
|
|
95
|
+
seen = set(column_data[1:])
|
|
96
|
+
if first_element in seen:
|
|
97
|
+
raise ValueError('Invalid table data. Column contains duplicate values.')
|
|
98
|
+
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class PanesValidator(BaseModel):
|
|
103
|
+
freeze: bool = Field(default=False, strict=True)
|
|
104
|
+
split: bool = Field(default=False, strict=True)
|
|
105
|
+
x_split: int = Field(default=0, strict=True)
|
|
106
|
+
y_split: int = Field(default=0, strict=True)
|
|
107
|
+
top_left_cell: str = Field(default='', strict=True)
|
|
108
|
+
active_pane: Literal['bottomLeft', 'bottomRight', 'topLeft', 'topRight', ''] = ''
|
|
109
|
+
selection: Optional[SetPanesSelection | list[Selection] | Selection] = None
|
|
110
|
+
|
|
111
|
+
@model_validator(mode='after')
|
|
112
|
+
def validate_panes(self) -> Self:
|
|
113
|
+
if self.x_split < 0 or self.y_split < 0:
|
|
114
|
+
raise ValueError('Split position should be positive.')
|
|
115
|
+
if self.top_left_cell != '':
|
|
116
|
+
_validate_cell_reference(self.top_left_cell)
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class DataValidationValidator(BaseModel):
|
|
121
|
+
sq_ref: str = Field(default=False, strict=True)
|
|
122
|
+
set_range: Optional[list[int | float]] = None
|
|
123
|
+
input_msg: Optional[list[str]] = None
|
|
124
|
+
drop_list: Optional[list[str | int | float] | str] = None
|
|
125
|
+
error_msg: Optional[list[str]] = None
|
|
126
|
+
|
|
127
|
+
@field_validator('sq_ref')
|
|
128
|
+
@classmethod
|
|
129
|
+
def validate_style_name(cls, sq_ref: str) -> str:
|
|
130
|
+
if ':' in sq_ref:
|
|
131
|
+
sq_ref_list = sq_ref.split(':')
|
|
132
|
+
_validate_cell_reference(sq_ref_list[0])
|
|
133
|
+
_validate_cell_reference(sq_ref_list[1])
|
|
134
|
+
else:
|
|
135
|
+
_validate_cell_reference(sq_ref)
|
|
136
|
+
return sq_ref
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class AutoFilterValidator(BaseModel):
|
|
140
|
+
target_range: str
|
|
141
|
+
|
|
142
|
+
@field_validator('target_range')
|
|
143
|
+
@classmethod
|
|
144
|
+
def validate_target_range(cls, target_range: str) -> str:
|
|
145
|
+
if ':' not in target_range:
|
|
146
|
+
raise ValueError('Invalid target range. Target range should be in the format "A1:B2".')
|
|
147
|
+
target_list = target_range.split(':')
|
|
148
|
+
if len(target_list) != 2:
|
|
149
|
+
raise ValueError('Invalid target range. Target range should be in the format "A1:B2".')
|
|
150
|
+
_validate_cell_reference(target_list[0])
|
|
151
|
+
_validate_cell_reference(target_list[1])
|
|
152
|
+
|
|
153
|
+
return target_range
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class CommentValidator(BaseModel):
|
|
157
|
+
cell: str
|
|
158
|
+
author: str
|
|
159
|
+
text: CommentTextStructure | CommentText | list[CommentText]
|
|
160
|
+
|
|
161
|
+
@field_validator('cell')
|
|
162
|
+
@classmethod
|
|
163
|
+
def validate_cell(cls, cell: str) -> str:
|
|
164
|
+
_validate_cell_reference(cell)
|
|
165
|
+
return cell
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Register validators and use them in the validate_call decorator
|
|
169
|
+
VALIDATORS = {
|
|
170
|
+
'create_table': TableValidator,
|
|
171
|
+
'set_panes': PanesValidator,
|
|
172
|
+
'set_data_validation': DataValidationValidator,
|
|
173
|
+
'auto_filter': AutoFilterValidator,
|
|
174
|
+
'add_comment': CommentValidator,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def validate_call(func):
|
|
179
|
+
def wrapper(*args, **kwargs):
|
|
180
|
+
func_name = func.__name__
|
|
181
|
+
if func_name not in VALIDATORS:
|
|
182
|
+
logger.warning(f'No validator found for function {func_name}. Skipping validation.')
|
|
183
|
+
return func(*args, **kwargs)
|
|
184
|
+
|
|
185
|
+
validator = VALIDATORS[func_name]
|
|
186
|
+
model_fields = validator.model_fields
|
|
187
|
+
has_self = 'self' in func.__code__.co_varnames
|
|
188
|
+
|
|
189
|
+
actual_args = args[1:] if has_self else args
|
|
190
|
+
|
|
191
|
+
_kwargs = dict(zip(model_fields, actual_args), **kwargs)
|
|
192
|
+
validator(**_kwargs)
|
|
193
|
+
|
|
194
|
+
return func(*args, **kwargs)
|
|
195
|
+
|
|
196
|
+
return wrapper
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Literal, Optional, overload
|
|
3
|
+
from typing import Literal, Optional, overload, List
|
|
4
4
|
|
|
5
5
|
from pyfastexcel.driver import ExcelDriver, WorkSheet
|
|
6
6
|
from pyfastexcel.utils import deprecated_warning
|
|
7
7
|
|
|
8
|
+
from pydantic import validate_call as pydantic_validate_call
|
|
9
|
+
|
|
10
|
+
from ._typing import CommentTextStructure, SetPanesSelection
|
|
11
|
+
from .utils import CommentText, Selection
|
|
12
|
+
|
|
8
13
|
|
|
9
14
|
class Workbook(ExcelDriver):
|
|
10
15
|
"""
|
|
@@ -102,6 +107,7 @@ class Workbook(ExcelDriver):
|
|
|
102
107
|
self._check_if_sheet_exists(sheet_name)
|
|
103
108
|
self.sheet = sheet_name
|
|
104
109
|
|
|
110
|
+
@pydantic_validate_call
|
|
105
111
|
def set_file_props(self, key: str, value: str) -> None:
|
|
106
112
|
"""
|
|
107
113
|
Sets a file property.
|
|
@@ -117,6 +123,7 @@ class Workbook(ExcelDriver):
|
|
|
117
123
|
raise ValueError(f'Invalid file property: {key}')
|
|
118
124
|
self.file_props[key] = value
|
|
119
125
|
|
|
126
|
+
@pydantic_validate_call
|
|
120
127
|
def protect_workbook(
|
|
121
128
|
self,
|
|
122
129
|
algorithm: str,
|
|
@@ -194,7 +201,7 @@ class Workbook(ExcelDriver):
|
|
|
194
201
|
y_split: int = 0,
|
|
195
202
|
top_left_cell: str = '',
|
|
196
203
|
active_pane: Literal['bottomLeft', 'bottomRight', 'topLeft', 'topRight', ''] = '',
|
|
197
|
-
selection: list[
|
|
204
|
+
selection: Optional[SetPanesSelection | list[Selection] | Selection] = None,
|
|
198
205
|
) -> None:
|
|
199
206
|
self._check_if_sheet_exists(sheet)
|
|
200
207
|
self.workbook[sheet].set_panes(
|
|
@@ -211,10 +218,10 @@ class Workbook(ExcelDriver):
|
|
|
211
218
|
self,
|
|
212
219
|
sheet: str,
|
|
213
220
|
sq_ref: str = '',
|
|
214
|
-
set_range: list[int | float] = None,
|
|
215
|
-
input_msg: list[str] = None,
|
|
216
|
-
drop_list: list[str] | str = None,
|
|
217
|
-
error_msg: list[str] = None,
|
|
221
|
+
set_range: Optional[list[int | float]] = None,
|
|
222
|
+
input_msg: Optional[list[str]] = None,
|
|
223
|
+
drop_list: Optional[list[str | int | float] | str] = None,
|
|
224
|
+
error_msg: Optional[list[str]] = None,
|
|
218
225
|
):
|
|
219
226
|
self._check_if_sheet_exists(sheet)
|
|
220
227
|
self.workbook[sheet].set_data_validation(
|
|
@@ -230,7 +237,7 @@ class Workbook(ExcelDriver):
|
|
|
230
237
|
sheet: str,
|
|
231
238
|
cell: str,
|
|
232
239
|
author: str,
|
|
233
|
-
text:
|
|
240
|
+
text: CommentTextStructure | CommentText | List[CommentText],
|
|
234
241
|
) -> None:
|
|
235
242
|
"""
|
|
236
243
|
Adds a comment to the specified cell.
|
|
@@ -282,3 +289,27 @@ class Workbook(ExcelDriver):
|
|
|
282
289
|
hidden,
|
|
283
290
|
engine,
|
|
284
291
|
)
|
|
292
|
+
|
|
293
|
+
def create_table(
|
|
294
|
+
self,
|
|
295
|
+
sheet: str,
|
|
296
|
+
cell_range: str,
|
|
297
|
+
name: str,
|
|
298
|
+
style_name: str = '',
|
|
299
|
+
show_first_column: bool = True,
|
|
300
|
+
show_last_column: bool = True,
|
|
301
|
+
show_row_stripes: bool = False,
|
|
302
|
+
show_column_stripes: bool = True,
|
|
303
|
+
validate_table: bool = True,
|
|
304
|
+
):
|
|
305
|
+
self._check_if_sheet_exists(sheet)
|
|
306
|
+
self.workbook[self.sheet].create_table(
|
|
307
|
+
cell_range,
|
|
308
|
+
name,
|
|
309
|
+
style_name,
|
|
310
|
+
show_first_column,
|
|
311
|
+
show_last_column,
|
|
312
|
+
show_row_stripes,
|
|
313
|
+
show_column_stripes,
|
|
314
|
+
validate_table,
|
|
315
|
+
)
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any, Literal, Optional, List, overload
|
|
4
4
|
|
|
5
5
|
from openpyxl_style_writer import CustomStyle
|
|
6
|
+
from pydantic import validate_call as pydantic_validate_call
|
|
6
7
|
|
|
7
8
|
from .style import StyleManager
|
|
8
9
|
from ._typing import CommentTextStructure, SetPanesSelection
|
|
@@ -10,7 +11,6 @@ from .utils import (
|
|
|
10
11
|
CommentText,
|
|
11
12
|
Selection,
|
|
12
13
|
_separate_alpha_numeric,
|
|
13
|
-
_validate_cell_reference,
|
|
14
14
|
column_to_index,
|
|
15
15
|
deprecated_warning,
|
|
16
16
|
cell_reference_to_index,
|
|
@@ -18,6 +18,8 @@ from .utils import (
|
|
|
18
18
|
validate_and_register_style,
|
|
19
19
|
transfer_string_slice_to_slice,
|
|
20
20
|
)
|
|
21
|
+
from .validators import validate_call
|
|
22
|
+
from .serializers import CommentSerializer, PanesSerializer, DataValidationSerializer
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class WorkSheetBase:
|
|
@@ -71,6 +73,7 @@ class WorkSheetBase:
|
|
|
71
73
|
self._data_validation_list = []
|
|
72
74
|
self._grouped_columns_list = []
|
|
73
75
|
self._grouped_rows_list = []
|
|
76
|
+
self._table_list = []
|
|
74
77
|
# Using pyfastexcel to write as default
|
|
75
78
|
self._engine: Literal['pyfastexcel', 'openpyxl'] = 'pyfastexcel'
|
|
76
79
|
|
|
@@ -131,8 +134,10 @@ class WorkSheetBase:
|
|
|
131
134
|
|
|
132
135
|
def _expand_row_and_cols(self, target_row: int, target_col: int) -> None:
|
|
133
136
|
data_row_len = len(self._data)
|
|
137
|
+
d = ()
|
|
134
138
|
if data_row_len == 0:
|
|
135
|
-
self._data.append([('', 'DEFAULT_STYLE')])
|
|
139
|
+
# self._data.append([('', 'DEFAULT_STYLE')])
|
|
140
|
+
self._data.append([d])
|
|
136
141
|
data_row_len = 1
|
|
137
142
|
data_col_len = len(self._data[0])
|
|
138
143
|
|
|
@@ -144,17 +149,14 @@ class WorkSheetBase:
|
|
|
144
149
|
if data_row_len > target_row:
|
|
145
150
|
if data_col_len <= target_col:
|
|
146
151
|
self._data[target_row].extend(
|
|
147
|
-
[
|
|
152
|
+
[d for _ in range(target_col + 1 - data_col_len)],
|
|
148
153
|
)
|
|
149
154
|
else:
|
|
150
155
|
current_row = max(data_row_len, target_row + 1)
|
|
151
156
|
current_col = max(data_col_len, target_col + 1)
|
|
152
|
-
default_value = ('', 'DEFAULT_STYLE')
|
|
157
|
+
# default_value = ('', 'DEFAULT_STYLE')
|
|
153
158
|
self._data.extend(
|
|
154
|
-
[
|
|
155
|
-
[default_value for _ in range(current_col)]
|
|
156
|
-
for _ in range(current_row - data_row_len)
|
|
157
|
-
],
|
|
159
|
+
[[d for _ in range(current_col)] for _ in range(current_row - data_row_len)],
|
|
158
160
|
)
|
|
159
161
|
|
|
160
162
|
def _transfer_to_dict(self) -> dict[str, Any]:
|
|
@@ -170,6 +172,7 @@ class WorkSheetBase:
|
|
|
170
172
|
'Comment': self._comment_list,
|
|
171
173
|
'GroupedRow': self._grouped_rows_list,
|
|
172
174
|
'GroupedCol': self._grouped_columns_list,
|
|
175
|
+
'Table': self._table_list,
|
|
173
176
|
}
|
|
174
177
|
return self._sheet
|
|
175
178
|
|
|
@@ -186,6 +189,7 @@ class WorkSheetBase:
|
|
|
186
189
|
'Comment': [],
|
|
187
190
|
'GroupedRow': [],
|
|
188
191
|
'GroupedCol': [],
|
|
192
|
+
'Table': [],
|
|
189
193
|
}
|
|
190
194
|
|
|
191
195
|
def _validate_value_and_set_default(self, value: Any):
|
|
@@ -224,7 +228,7 @@ class WorkSheetBase:
|
|
|
224
228
|
'Style should be a string or CustomStyle object.',
|
|
225
229
|
)
|
|
226
230
|
# The case that user do not register the Custom Style by 'Class attributes'
|
|
227
|
-
# or
|
|
231
|
+
# or set_custom_style function.
|
|
228
232
|
if (
|
|
229
233
|
isinstance(value[1], CustomStyle)
|
|
230
234
|
and StyleManager._STYLE_NAME_MAP.get(value[1]) is None
|
|
@@ -352,7 +356,7 @@ class WorkSheet(WorkSheetBase):
|
|
|
352
356
|
self,
|
|
353
357
|
row: int,
|
|
354
358
|
column: int,
|
|
355
|
-
value:
|
|
359
|
+
value: Any,
|
|
356
360
|
style: str | CustomStyle = 'DEFAULT_STYLE',
|
|
357
361
|
) -> None:
|
|
358
362
|
"""
|
|
@@ -420,6 +424,7 @@ class WorkSheet(WorkSheetBase):
|
|
|
420
424
|
else:
|
|
421
425
|
raise TypeError('Target should be a string, slice, or list[row, index].')
|
|
422
426
|
|
|
427
|
+
@pydantic_validate_call
|
|
423
428
|
def set_cell_width(self, col: str | int, value: int) -> None:
|
|
424
429
|
if isinstance(col, str):
|
|
425
430
|
col = column_to_index(col)
|
|
@@ -427,6 +432,7 @@ class WorkSheet(WorkSheetBase):
|
|
|
427
432
|
raise ValueError(f'Invalid column index: {col}')
|
|
428
433
|
self._width_dict[col] = value
|
|
429
434
|
|
|
435
|
+
@pydantic_validate_call
|
|
430
436
|
def set_cell_height(self, row: int, value: int) -> None:
|
|
431
437
|
if row < 1 or row > 1048576:
|
|
432
438
|
raise ValueError(f'Invalid row index: {row}')
|
|
@@ -532,6 +538,7 @@ class WorkSheet(WorkSheetBase):
|
|
|
532
538
|
|
|
533
539
|
self._merged_cells_list.append((top_left_cell, bottom_right_cell))
|
|
534
540
|
|
|
541
|
+
@validate_call
|
|
535
542
|
def auto_filter(self, target_range: str) -> None:
|
|
536
543
|
"""
|
|
537
544
|
Sets the auto filter for the specified range.
|
|
@@ -545,13 +552,9 @@ class WorkSheet(WorkSheetBase):
|
|
|
545
552
|
Returns:
|
|
546
553
|
None
|
|
547
554
|
"""
|
|
548
|
-
if ':' not in target_range:
|
|
549
|
-
raise ValueError('Invalid target range. Target range should be in the format "A1:B2".')
|
|
550
|
-
target_list = target_range.split(':')
|
|
551
|
-
_validate_cell_reference(target_list[0])
|
|
552
|
-
_validate_cell_reference(target_list[1])
|
|
553
555
|
self._auto_filter_set.add(target_range)
|
|
554
556
|
|
|
557
|
+
@validate_call
|
|
555
558
|
def set_panes(
|
|
556
559
|
self,
|
|
557
560
|
freeze: bool = False,
|
|
@@ -560,7 +563,7 @@ class WorkSheet(WorkSheetBase):
|
|
|
560
563
|
y_split: int = 0,
|
|
561
564
|
top_left_cell: str = '',
|
|
562
565
|
active_pane: Literal['bottomLeft', 'bottomRight', 'topLeft', 'topRight', ''] = '',
|
|
563
|
-
selection: Optional[SetPanesSelection | list[Selection]] = None,
|
|
566
|
+
selection: Optional[SetPanesSelection | list[Selection] | Selection] = None,
|
|
564
567
|
) -> None:
|
|
565
568
|
"""
|
|
566
569
|
Sets the panes for the worksheet with options for freezing, splitting, and selection.
|
|
@@ -583,21 +586,7 @@ class WorkSheet(WorkSheetBase):
|
|
|
583
586
|
Returns:
|
|
584
587
|
None
|
|
585
588
|
"""
|
|
586
|
-
|
|
587
|
-
raise ValueError('Split position should be positive.')
|
|
588
|
-
if top_left_cell != '':
|
|
589
|
-
_validate_cell_reference(top_left_cell)
|
|
590
|
-
if active_pane not in ['bottomLeft', 'bottomRight', 'topLeft', 'topRight', '']:
|
|
591
|
-
raise ValueError(
|
|
592
|
-
'Invalid active pane. The options are bottomLeft, bottomRight, topLeft, topRight.',
|
|
593
|
-
)
|
|
594
|
-
|
|
595
|
-
if selection is None:
|
|
596
|
-
selection = []
|
|
597
|
-
elif not isinstance(selection, list):
|
|
598
|
-
selection = [selection]
|
|
599
|
-
|
|
600
|
-
selection = [item.to_dict() for item in selection if isinstance(item, Selection)]
|
|
589
|
+
selection = PanesSerializer.serialize_selection(selection)
|
|
601
590
|
|
|
602
591
|
self._panes_dict = {
|
|
603
592
|
'freeze': freeze,
|
|
@@ -609,12 +598,13 @@ class WorkSheet(WorkSheetBase):
|
|
|
609
598
|
'selection': selection,
|
|
610
599
|
}
|
|
611
600
|
|
|
601
|
+
@validate_call
|
|
612
602
|
def set_data_validation(
|
|
613
603
|
self,
|
|
614
604
|
sq_ref: str = '',
|
|
615
605
|
set_range: Optional[list[int | float]] = None,
|
|
616
606
|
input_msg: Optional[list[str]] = None,
|
|
617
|
-
drop_list: Optional[list[str] | str] = None,
|
|
607
|
+
drop_list: Optional[list[str | int | float] | str] = None,
|
|
618
608
|
error_msg: Optional[list[str]] = None,
|
|
619
609
|
):
|
|
620
610
|
"""
|
|
@@ -633,54 +623,16 @@ class WorkSheet(WorkSheetBase):
|
|
|
633
623
|
Returns:
|
|
634
624
|
None
|
|
635
625
|
"""
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
drop_list_key = 'drop_list'
|
|
644
|
-
if isinstance(drop_list, str):
|
|
645
|
-
if ':' not in drop_list:
|
|
646
|
-
raise ValueError(
|
|
647
|
-
'Invalid drop list. Sequential Reference'
|
|
648
|
-
'Drop list should be in the format "A1:B2".',
|
|
649
|
-
)
|
|
650
|
-
drop_list_split = drop_list.split(':')
|
|
651
|
-
_validate_cell_reference(drop_list_split[0])
|
|
652
|
-
_validate_cell_reference(drop_list_split[1])
|
|
653
|
-
drop_list_key = 'sqref_drop_list'
|
|
654
|
-
elif drop_list is not None:
|
|
655
|
-
if not isinstance(drop_list, list):
|
|
656
|
-
raise ValueError('Drop list should be a list of strings.')
|
|
657
|
-
drop_list = [str(x) for x in drop_list]
|
|
658
|
-
|
|
659
|
-
dv = {}
|
|
626
|
+
dv = DataValidationSerializer(
|
|
627
|
+
set_range=set_range,
|
|
628
|
+
input_msg=input_msg,
|
|
629
|
+
drop_list=drop_list,
|
|
630
|
+
error_msg=error_msg,
|
|
631
|
+
).model_dump()
|
|
660
632
|
dv['sq_ref'] = sq_ref
|
|
661
|
-
if set_range is not None:
|
|
662
|
-
if not isinstance(set_range, list) or len(set_range) != 2:
|
|
663
|
-
raise ValueError('Set range should be a list of two elements. Like [1, 10].')
|
|
664
|
-
dv['set_range'] = set_range
|
|
665
|
-
if input_msg is not None:
|
|
666
|
-
if not isinstance(input_msg, list) or len(input_msg) != 2:
|
|
667
|
-
raise ValueError(
|
|
668
|
-
'Input message should be a list of two elements. Like ["Title", "Body"].',
|
|
669
|
-
)
|
|
670
|
-
dv['input_title'] = input_msg[0]
|
|
671
|
-
dv['input_body'] = input_msg[1]
|
|
672
|
-
if drop_list is not None:
|
|
673
|
-
dv[drop_list_key] = drop_list
|
|
674
|
-
if error_msg is not None:
|
|
675
|
-
if not isinstance(error_msg, list) or len(error_msg) != 2:
|
|
676
|
-
raise ValueError(
|
|
677
|
-
'Error message should be a list of two elements. Like ["Title", "Body"].',
|
|
678
|
-
)
|
|
679
|
-
dv['error_title'] = error_msg[0]
|
|
680
|
-
dv['error_body'] = error_msg[1]
|
|
681
|
-
|
|
682
633
|
self._data_validation_list.append(dv)
|
|
683
634
|
|
|
635
|
+
@validate_call
|
|
684
636
|
def add_comment(
|
|
685
637
|
self,
|
|
686
638
|
cell: str,
|
|
@@ -698,30 +650,11 @@ class WorkSheet(WorkSheetBase):
|
|
|
698
650
|
Returns:
|
|
699
651
|
None
|
|
700
652
|
"""
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
[text]
|
|
704
|
-
if isinstance(text, (str, CommentText))
|
|
705
|
-
else text
|
|
706
|
-
if isinstance(text, list)
|
|
707
|
-
else [text]
|
|
708
|
-
)
|
|
709
|
-
if all(isinstance(item, (dict, str)) for item in text):
|
|
710
|
-
for idx, item in enumerate(text):
|
|
711
|
-
if isinstance(item, str):
|
|
712
|
-
text[idx] = {'text': item}
|
|
713
|
-
else:
|
|
714
|
-
if 'text' not in item:
|
|
715
|
-
raise ValueError('Comment text should contain the key "text".')
|
|
716
|
-
text[idx] = {
|
|
717
|
-
k[0].upper() + k[1:] if k != 'text' else k: v for k, v in item.items()
|
|
718
|
-
}
|
|
719
|
-
elif all(isinstance(item, CommentText) for item in text):
|
|
720
|
-
text = [t.to_dict() for t in text]
|
|
721
|
-
else:
|
|
722
|
-
raise ValueError('Comment text should be a string or a list of dictionaries.')
|
|
653
|
+
text = CommentSerializer.serialize_text(text)
|
|
654
|
+
|
|
723
655
|
self._comment_list.append({'cell': cell, 'author': author, 'paragraph': text})
|
|
724
656
|
|
|
657
|
+
@pydantic_validate_call
|
|
725
658
|
def group_columns(
|
|
726
659
|
self,
|
|
727
660
|
start_col: str,
|
|
@@ -756,6 +689,7 @@ class WorkSheet(WorkSheetBase):
|
|
|
756
689
|
)
|
|
757
690
|
self._engine = engine
|
|
758
691
|
|
|
692
|
+
@pydantic_validate_call
|
|
759
693
|
def group_rows(
|
|
760
694
|
self,
|
|
761
695
|
start_row: int,
|
|
@@ -789,3 +723,47 @@ class WorkSheet(WorkSheetBase):
|
|
|
789
723
|
}
|
|
790
724
|
)
|
|
791
725
|
self._engine = engine
|
|
726
|
+
|
|
727
|
+
@validate_call
|
|
728
|
+
def create_table(
|
|
729
|
+
self,
|
|
730
|
+
cell_range: str,
|
|
731
|
+
name: str,
|
|
732
|
+
style_name: str = '',
|
|
733
|
+
show_first_column: bool = True,
|
|
734
|
+
show_last_column: bool = True,
|
|
735
|
+
show_row_stripes: bool = False,
|
|
736
|
+
show_column_stripes: bool = True,
|
|
737
|
+
validate_table: bool = True,
|
|
738
|
+
):
|
|
739
|
+
"""
|
|
740
|
+
Creates a table within the specified cell range with given style and display options.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
cell_range (str): The cell range where the table should be created (e.g., 'A1:D10').
|
|
744
|
+
name (str): The name of the table.
|
|
745
|
+
style_name (str): The style to apply to the table. Defaults to an empty string, which
|
|
746
|
+
applies the default style.
|
|
747
|
+
show_first_column (bool): Whether to emphasize the first column.
|
|
748
|
+
show_last_column (bool): Whether to emphasize the last column.
|
|
749
|
+
show_row_stripes (bool): Whether to show row stripes for alternate row shading.
|
|
750
|
+
show_column_stripes (bool): Whether to show column stripes for alternate column shading.
|
|
751
|
+
validate_table (bool): Whether to validate the table through TableFinalValidation.
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
None
|
|
755
|
+
"""
|
|
756
|
+
table = {
|
|
757
|
+
'range': cell_range,
|
|
758
|
+
'name': name,
|
|
759
|
+
'style_name': style_name,
|
|
760
|
+
'show_first_column': show_first_column,
|
|
761
|
+
'show_last_column': show_last_column,
|
|
762
|
+
'show_row_stripes': show_row_stripes,
|
|
763
|
+
'show_column_stripes': show_column_stripes,
|
|
764
|
+
# validate_table is a flag to decide whether
|
|
765
|
+
# to validate the table by TableFinalValidation
|
|
766
|
+
'validate_table': validate_table,
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
self._table_list.append(table)
|
|
@@ -133,14 +133,12 @@ class StreamWriter(Workbook):
|
|
|
133
133
|
elif isinstance(style, str):
|
|
134
134
|
style = self._handle_string_style(style, kwargs)
|
|
135
135
|
|
|
136
|
+
value = tuple((validate_and_format_value(x, set_default_style=False), style) for x in value)
|
|
137
|
+
|
|
136
138
|
if create_row:
|
|
137
|
-
value = tuple(
|
|
138
|
-
(validate_and_format_value(x, set_default_style=False), style) for x in value
|
|
139
|
-
)
|
|
140
139
|
self.workbook[self.sheet].data.append(value)
|
|
141
140
|
else:
|
|
142
|
-
|
|
143
|
-
self._row_list.append((value, style))
|
|
141
|
+
self._row_list.extend(value)
|
|
144
142
|
|
|
145
143
|
def create_row(self):
|
|
146
144
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyfastexcel
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: High performace excel file generation library.
|
|
5
5
|
Home-page: https://github.com/Zncl2222/pyfastexcel
|
|
6
6
|
Author: Zncl2222
|
|
@@ -21,6 +21,8 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: msgspec
|
|
23
23
|
Requires-Dist: openpyxl-style-writer>=1.1.3
|
|
24
|
+
Requires-Dist: pydantic
|
|
25
|
+
Requires-Dist: eval-type-backport; python_version < "3.9"
|
|
24
26
|
|
|
25
27
|
# pyfastexcel
|
|
26
28
|
|
|
@@ -8,8 +8,10 @@ pyfastexcel/driver.py
|
|
|
8
8
|
pyfastexcel/logformatter.py
|
|
9
9
|
pyfastexcel/pyfastexcel.dll
|
|
10
10
|
pyfastexcel/pyfastexcel.so
|
|
11
|
+
pyfastexcel/serializers.py
|
|
11
12
|
pyfastexcel/style.py
|
|
12
13
|
pyfastexcel/utils.py
|
|
14
|
+
pyfastexcel/validators.py
|
|
13
15
|
pyfastexcel/workbook.py
|
|
14
16
|
pyfastexcel/worksheet.py
|
|
15
17
|
pyfastexcel/writer.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = pyfastexcel
|
|
3
|
-
version = 0.1.
|
|
3
|
+
version = 0.1.4
|
|
4
4
|
description = High performace excel file generation library.
|
|
5
5
|
long_description = file: README.md
|
|
6
6
|
long_description_content_type = text/markdown
|
|
@@ -27,6 +27,8 @@ packages =
|
|
|
27
27
|
install_requires =
|
|
28
28
|
msgspec
|
|
29
29
|
openpyxl-style-writer>=1.1.3
|
|
30
|
+
pydantic
|
|
31
|
+
eval-type-backport;python_version < '3.9'
|
|
30
32
|
python_requires = >=3.7
|
|
31
33
|
include_package_data = true
|
|
32
34
|
zip_safe = false
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|