pyfastexcel 0.1.8__py3-none-win_amd64.whl

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/utils.py ADDED
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import re
5
+ import string
6
+ import warnings
7
+
8
+ # from dataclasses import dataclass
9
+ from pydantic.dataclasses import dataclass
10
+ from typing import Any, Literal
11
+
12
+ from openpyxl_style_writer import CustomStyle
13
+
14
+ warnings.simplefilter('always', DeprecationWarning)
15
+
16
+
17
+ @dataclass
18
+ class CommentText:
19
+ text: str
20
+ size: int | None = None
21
+ name: str | None = None
22
+ bold: bool | None = None
23
+ italic: bool | None = None
24
+ underline: Literal['single', 'double'] | None = None
25
+ strike: bool | None = None
26
+ vert_align: str | None = None
27
+ color: str | None = None
28
+
29
+ def to_dict(self):
30
+ result = {
31
+ k[0].upper() + k[1:]: v
32
+ for k, v in self.__dict__.items()
33
+ if v is not None and k != 'text'
34
+ }
35
+ if result.get('Vert_align') is not None:
36
+ result['VertAlign'] = result['Vert_align']
37
+ result['text'] = self.text
38
+ return result
39
+
40
+
41
+ @dataclass
42
+ class Selection:
43
+ sq_ref: str
44
+ active_cell: str
45
+ pane: str
46
+
47
+ def to_dict(self):
48
+ return {
49
+ 'sq_ref': self.sq_ref,
50
+ 'active_cell': self.active_cell,
51
+ 'pane': self.pane,
52
+ }
53
+
54
+
55
+ def set_debug_level(level: int):
56
+ logging.basicConfig(level=level)
57
+
58
+
59
+ def deprecated_warning(msg: str):
60
+ warnings.warn(
61
+ msg,
62
+ DeprecationWarning,
63
+ stacklevel=2,
64
+ )
65
+
66
+
67
+ def set_custom_style(style_name: str, style: CustomStyle) -> None:
68
+ from .style import StyleManager
69
+
70
+ StyleManager.set_custom_style(style_name, style)
71
+
72
+
73
+ def validate_and_register_style(style: CustomStyle) -> None:
74
+ from .style import StyleManager
75
+
76
+ if not isinstance(style, CustomStyle):
77
+ raise TypeError(
78
+ f'Invalid type ({type(style)}). Style should be a CustomStyle object.',
79
+ )
80
+ set_custom_style(f'Custom Style {StyleManager._STYLE_ID}', style)
81
+ StyleManager._STYLE_ID += 1
82
+
83
+
84
+ def validate_and_format_value(
85
+ value: Any,
86
+ set_default_style: bool = True,
87
+ ) -> tuple[Any, Literal['DEFAULT_STYLE']] | Any:
88
+ # Convert non-numeric value to string
89
+ value = f'{value}' if not isinstance(value, (int, float, str)) else value
90
+ # msgpec does not support np.float64, so we should convert
91
+ # it to python float.
92
+ value = float(value) if isinstance(value, float) else value
93
+
94
+ return (value, 'DEFAULT_STYLE') if set_default_style else value
95
+
96
+
97
+ def transfer_string_slice_to_slice(string_slice: str) -> slice:
98
+ '''
99
+ Transfer a string slice to a slice object.
100
+ For example, 'A1:B2' will be transfered to slice(1, 2, 1).
101
+ '''
102
+ start, end = string_slice.split(':')
103
+ alpha_start, row_start = _separate_alpha_numeric(start)
104
+ alpha_end, row_end = _separate_alpha_numeric(end)
105
+ return slice(f'{alpha_start}{row_start}', f'{alpha_end}{row_end}')
106
+
107
+
108
+ def _separate_alpha_numeric(input_string: str) -> tuple[str, int]:
109
+ '''
110
+ Separate the alpha and numeric part of a string.
111
+ Return alpha_part at first index and num_part at second index.
112
+ '''
113
+ alpha_part = re.findall(r'[a-zA-Z]+', input_string)
114
+ num_part = re.findall(r'[0-9]+', input_string)
115
+ if len(alpha_part) == 0 or len(num_part) == 0:
116
+ raise ValueError(f'Invalid input string {input_string}.')
117
+ return alpha_part[0], int(num_part[0])
118
+
119
+
120
+ def _is_valid_column(column: str) -> bool:
121
+ """
122
+ Validate the alphabet part of the column.
123
+ """
124
+ column = column.upper()
125
+ index = 0
126
+ for c in column:
127
+ index = index * 26 + (ord(c) - ord('A')) + 1
128
+ return 1 <= index <= 16384
129
+
130
+
131
+ def column_to_index(column: str) -> int:
132
+ """
133
+ Translate the column name to the column index.
134
+ """
135
+ if not isinstance(column, str):
136
+ raise TypeError(f'Invalid type ({type(column)}). Column should be a string.')
137
+ if len(column) > 3:
138
+ raise ValueError(f"Invalid column ({column}). Maximum Column is 'XFD'.")
139
+ if not all(c in string.ascii_uppercase for c in column):
140
+ raise ValueError(f'Invalid column ({column}). Column should be in uppercase.')
141
+ if not _is_valid_column(column):
142
+ raise ValueError(f"Invalid column ({column}). Maximum Column is 'XFD'.")
143
+ column = column.upper()
144
+ index = 0
145
+ for c in column:
146
+ index = index * 26 + (ord(c) - ord('A')) + 1
147
+ return index
148
+
149
+
150
+ def index_to_column(index: int) -> str:
151
+ """
152
+ Translate the index to the column name.
153
+ """
154
+ if not isinstance(index, int):
155
+ raise TypeError(f'Invalid type ({type(index)}). Index should be a string.')
156
+ if index < 1 or index > 16384:
157
+ raise ValueError(f'Invalid index ({index}). Index should less and equal to 16384.')
158
+ name = ''
159
+ while index > 0:
160
+ index, r = divmod(index - 1, 26)
161
+ name = chr(r + ord('A')) + name
162
+ return name
163
+
164
+
165
+ def cell_reference_to_index(index: str) -> tuple[int, int]:
166
+ """
167
+ Return the row and column index of the given Excel cell reference.
168
+ """
169
+ alpha, num = _separate_alpha_numeric(index)
170
+ column = column_to_index(alpha)
171
+ row = int(num)
172
+ return row - 1, column - 1
173
+
174
+
175
+ def _validate_cell_reference(index: str) -> bool:
176
+ alpha, num = _separate_alpha_numeric(index)
177
+ num = int(num)
178
+ if not all(c in string.ascii_uppercase for c in alpha):
179
+ raise ValueError(f'Invalid column ({alpha}). Column should be in uppercase.')
180
+ if not _is_valid_column(alpha):
181
+ raise ValueError(f"Invalid column ({alpha}). Maximum Column is 'XFD'.")
182
+ if num < 1 or num > 16384:
183
+ raise ValueError(f'Invalid index ({num}). Index should less and equal to 16384.')
184
+ return True
@@ -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