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.
Files changed (25) hide show
  1. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/PKG-INFO +3 -1
  2. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/_typing.py +3 -1
  3. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/driver.py +6 -0
  4. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/pyfastexcel.dll +0 -0
  5. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/pyfastexcel.so +0 -0
  6. pyfastexcel-0.1.4/pyfastexcel/serializers.py +100 -0
  7. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/utils.py +3 -1
  8. pyfastexcel-0.1.4/pyfastexcel/validators.py +196 -0
  9. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/workbook.py +38 -7
  10. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/worksheet.py +77 -99
  11. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/writer.py +3 -5
  12. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel.egg-info/PKG-INFO +3 -1
  13. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel.egg-info/SOURCES.txt +2 -0
  14. pyfastexcel-0.1.4/pyfastexcel.egg-info/requires.txt +6 -0
  15. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/setup.cfg +3 -1
  16. pyfastexcel-0.1.2/pyfastexcel.egg-info/requires.txt +0 -2
  17. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/LICENSE +0 -0
  18. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/README.md +0 -0
  19. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/__init__.py +0 -0
  20. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/logformatter.py +0 -0
  21. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel/style.py +0 -0
  22. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel.egg-info/dependency_links.txt +0 -0
  23. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel.egg-info/not-zip-safe +0 -0
  24. {pyfastexcel-0.1.2 → pyfastexcel-0.1.4}/pyfastexcel.egg-info/top_level.txt +0 -0
  25. {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.2
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, TypedDict, Optional, List, Union
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)
@@ -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
- from dataclasses import dataclass
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[dict[str, str]] = None,
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: str | dict[str, str] | list[str | dict[str, str]],
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
- [('', 'DEFAULT_STYLE') for _ in range(target_col + 1 - data_col_len)],
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 set_cumston_style function.
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: any,
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
- if x_split < 0 or y_split < 0:
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
- if ':' in sq_ref:
637
- sq_ref_list = sq_ref.split(':')
638
- _validate_cell_reference(sq_ref_list[0])
639
- _validate_cell_reference(sq_ref_list[1])
640
- else:
641
- _validate_cell_reference(sq_ref)
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
- _validate_cell_reference(cell)
702
- text = (
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
- value = validate_and_format_value(value, set_default_style=False)
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.2
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
@@ -0,0 +1,6 @@
1
+ msgspec
2
+ openpyxl-style-writer>=1.1.3
3
+ pydantic
4
+
5
+ [:python_version < "3.9"]
6
+ eval-type-backport
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = pyfastexcel
3
- version = 0.1.2
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
@@ -1,2 +0,0 @@
1
- msgspec
2
- openpyxl-style-writer>=1.1.3
File without changes
File without changes
File without changes