aspose-cells-foss 25.12.1__py3-none-any.whl → 26.2.2__py3-none-any.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.
- aspose_cells/__init__.py +88 -0
- aspose_cells/auto_filter.py +527 -0
- aspose_cells/cell.py +483 -0
- aspose_cells/cell_value_handler.py +319 -0
- aspose_cells/cells.py +779 -0
- aspose_cells/cfb_handler.py +445 -0
- aspose_cells/cfb_writer.py +659 -0
- aspose_cells/cfb_writer_minimal.py +337 -0
- aspose_cells/comment_xml.py +475 -0
- aspose_cells/conditional_format.py +1185 -0
- aspose_cells/csv_handler.py +690 -0
- aspose_cells/data_validation.py +911 -0
- aspose_cells/document_properties.py +356 -0
- aspose_cells/encryption_crypto.py +247 -0
- aspose_cells/encryption_params.py +138 -0
- aspose_cells/hyperlink.py +372 -0
- aspose_cells/json_handler.py +185 -0
- aspose_cells/markdown_handler.py +583 -0
- aspose_cells/shared_strings.py +101 -0
- aspose_cells/style.py +841 -0
- aspose_cells/workbook.py +499 -0
- aspose_cells/workbook_hash_password.py +68 -0
- aspose_cells/workbook_properties.py +712 -0
- aspose_cells/worksheet.py +570 -0
- aspose_cells/worksheet_properties.py +1239 -0
- aspose_cells/xlsx_encryptor.py +403 -0
- aspose_cells/xml_autofilter_loader.py +195 -0
- aspose_cells/xml_autofilter_saver.py +173 -0
- aspose_cells/xml_conditional_format_loader.py +215 -0
- aspose_cells/xml_conditional_format_saver.py +351 -0
- aspose_cells/xml_datavalidation_loader.py +239 -0
- aspose_cells/xml_datavalidation_saver.py +245 -0
- aspose_cells/xml_hyperlink_handler.py +323 -0
- aspose_cells/xml_loader.py +986 -0
- aspose_cells/xml_properties_loader.py +512 -0
- aspose_cells/xml_properties_saver.py +607 -0
- aspose_cells/xml_saver.py +1306 -0
- aspose_cells_foss-26.2.2.dist-info/METADATA +190 -0
- aspose_cells_foss-26.2.2.dist-info/RECORD +41 -0
- {aspose_cells_foss-25.12.1.dist-info → aspose_cells_foss-26.2.2.dist-info}/WHEEL +1 -1
- aspose_cells_foss-26.2.2.dist-info/top_level.txt +1 -0
- aspose/__init__.py +0 -14
- aspose/cells/__init__.py +0 -31
- aspose/cells/cell.py +0 -350
- aspose/cells/constants.py +0 -44
- aspose/cells/converters/__init__.py +0 -13
- aspose/cells/converters/csv_converter.py +0 -55
- aspose/cells/converters/json_converter.py +0 -46
- aspose/cells/converters/markdown_converter.py +0 -453
- aspose/cells/drawing/__init__.py +0 -17
- aspose/cells/drawing/anchor.py +0 -172
- aspose/cells/drawing/collection.py +0 -233
- aspose/cells/drawing/image.py +0 -338
- aspose/cells/formats.py +0 -80
- aspose/cells/formula/__init__.py +0 -10
- aspose/cells/formula/evaluator.py +0 -360
- aspose/cells/formula/functions.py +0 -433
- aspose/cells/formula/tokenizer.py +0 -340
- aspose/cells/io/__init__.py +0 -27
- aspose/cells/io/csv/__init__.py +0 -8
- aspose/cells/io/csv/reader.py +0 -88
- aspose/cells/io/csv/writer.py +0 -98
- aspose/cells/io/factory.py +0 -138
- aspose/cells/io/interfaces.py +0 -48
- aspose/cells/io/json/__init__.py +0 -8
- aspose/cells/io/json/reader.py +0 -126
- aspose/cells/io/json/writer.py +0 -119
- aspose/cells/io/md/__init__.py +0 -8
- aspose/cells/io/md/reader.py +0 -161
- aspose/cells/io/md/writer.py +0 -334
- aspose/cells/io/models.py +0 -64
- aspose/cells/io/xlsx/__init__.py +0 -9
- aspose/cells/io/xlsx/constants.py +0 -312
- aspose/cells/io/xlsx/image_writer.py +0 -311
- aspose/cells/io/xlsx/reader.py +0 -284
- aspose/cells/io/xlsx/writer.py +0 -931
- aspose/cells/plugins/__init__.py +0 -6
- aspose/cells/plugins/docling_backend/__init__.py +0 -7
- aspose/cells/plugins/docling_backend/backend.py +0 -535
- aspose/cells/plugins/markitdown_plugin/__init__.py +0 -15
- aspose/cells/plugins/markitdown_plugin/plugin.py +0 -128
- aspose/cells/range.py +0 -210
- aspose/cells/style.py +0 -287
- aspose/cells/utils/__init__.py +0 -54
- aspose/cells/utils/coordinates.py +0 -68
- aspose/cells/utils/exceptions.py +0 -43
- aspose/cells/utils/validation.py +0 -102
- aspose/cells/workbook.py +0 -352
- aspose/cells/worksheet.py +0 -670
- aspose_cells_foss-25.12.1.dist-info/METADATA +0 -189
- aspose_cells_foss-25.12.1.dist-info/RECORD +0 -53
- aspose_cells_foss-25.12.1.dist-info/entry_points.txt +0 -2
- aspose_cells_foss-25.12.1.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aspose.Cells for Python - XML Conditional Formatting Saver Module
|
|
3
|
+
|
|
4
|
+
This module provides the ConditionalFormatXMLWriter class which handles saving
|
|
5
|
+
conditional formatting data to XML format according to ECMA-376 specification.
|
|
6
|
+
|
|
7
|
+
ECMA-376 Section 18.3.1.18 defines the conditionalFormatting element structure.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConditionalFormatXMLWriter:
|
|
12
|
+
"""
|
|
13
|
+
Handles writing conditional formatting data to XML format for .xlsx files.
|
|
14
|
+
|
|
15
|
+
The ConditionalFormatXMLWriter class is responsible for creating the XML
|
|
16
|
+
representation of conditional formatting rules including cell value rules,
|
|
17
|
+
text rules, date rules, formula rules, color scales, data bars, icon sets,
|
|
18
|
+
and more.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
>>> writer = ConditionalFormatXMLWriter(escape_xml_func)
|
|
22
|
+
>>> xml = writer.format_conditional_formatting_xml(worksheet.conditional_formats)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, escape_xml_func):
|
|
26
|
+
"""
|
|
27
|
+
Initializes a new instance of the ConditionalFormatXMLWriter class.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
escape_xml_func: A function to escape XML special characters.
|
|
31
|
+
"""
|
|
32
|
+
self._escape_xml = escape_xml_func
|
|
33
|
+
|
|
34
|
+
def format_conditional_formatting_xml(self, conditional_formats):
|
|
35
|
+
"""
|
|
36
|
+
Formats conditional formatting as XML according to ECMA-376 specification.
|
|
37
|
+
|
|
38
|
+
ECMA-376 Section 18.3.1.18 defines the conditionalFormatting element structure:
|
|
39
|
+
- conditionalFormatting: Main element with sqref attribute (cell ranges)
|
|
40
|
+
- cfRule: Individual conditional formatting rule
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
conditional_formats (ConditionalFormatCollection): The collection of conditional formats.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
str: XML representation of the conditional formatting.
|
|
47
|
+
"""
|
|
48
|
+
xml = ''
|
|
49
|
+
|
|
50
|
+
# Group conditional formats by range (sqref)
|
|
51
|
+
formats_by_range = {}
|
|
52
|
+
for cf in conditional_formats:
|
|
53
|
+
if cf.range is None:
|
|
54
|
+
continue
|
|
55
|
+
if cf.range not in formats_by_range:
|
|
56
|
+
formats_by_range[cf.range] = []
|
|
57
|
+
formats_by_range[cf.range].append(cf)
|
|
58
|
+
|
|
59
|
+
# Write conditionalFormatting elements
|
|
60
|
+
for sqref, formats in formats_by_range.items():
|
|
61
|
+
xml += f' <conditionalFormatting sqref="{sqref}">\n'
|
|
62
|
+
|
|
63
|
+
for cf in formats:
|
|
64
|
+
xml += self._format_cf_rule_xml(cf)
|
|
65
|
+
|
|
66
|
+
xml += ' </conditionalFormatting>\n'
|
|
67
|
+
|
|
68
|
+
return xml
|
|
69
|
+
|
|
70
|
+
def _format_cf_rule_xml(self, cf):
|
|
71
|
+
"""
|
|
72
|
+
Formats a single conditional formatting rule as XML.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
cf (ConditionalFormat): The conditional format object.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
str: XML representation of the cfRule element.
|
|
79
|
+
"""
|
|
80
|
+
# Map user-friendly type names to ECMA-376 XML type names
|
|
81
|
+
type_map = {
|
|
82
|
+
'cellValue': 'cellIs',
|
|
83
|
+
'text': 'containsText',
|
|
84
|
+
'date': 'timePeriod',
|
|
85
|
+
'formula': 'expression',
|
|
86
|
+
'duplicateValues': 'duplicateValues',
|
|
87
|
+
'uniqueValues': 'uniqueValues',
|
|
88
|
+
'top10': 'top10',
|
|
89
|
+
'bottom10': 'top10', # ECMA-376: both use 'top10' type, distinguished by bottom attribute
|
|
90
|
+
'aboveAverage': 'aboveAverage',
|
|
91
|
+
'belowAverage': 'aboveAverage', # ECMA-376: both use 'aboveAverage' type, distinguished by aboveAverage attribute
|
|
92
|
+
'colorScale': 'colorScale',
|
|
93
|
+
'dataBar': 'dataBar',
|
|
94
|
+
'iconSet': 'iconSet',
|
|
95
|
+
'containsText': 'containsText',
|
|
96
|
+
'notContainsText': 'notContainsText',
|
|
97
|
+
'beginsWith': 'beginsWith',
|
|
98
|
+
'endsWith': 'endsWith',
|
|
99
|
+
'containsBlanks': 'containsBlanks',
|
|
100
|
+
'notContainsBlanks': 'notContainsBlanks',
|
|
101
|
+
'containsErrors': 'containsErrors',
|
|
102
|
+
'notContainsErrors': 'notContainsErrors',
|
|
103
|
+
'timePeriod': 'timePeriod',
|
|
104
|
+
'expression': 'expression',
|
|
105
|
+
'cellIs': 'cellIs'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
rule_type = type_map.get(cf._type, cf._type)
|
|
109
|
+
if rule_type is None:
|
|
110
|
+
return ''
|
|
111
|
+
|
|
112
|
+
# Build cfRule attributes
|
|
113
|
+
attrs = [f'type="{rule_type}"']
|
|
114
|
+
|
|
115
|
+
# Add dxfId if formatting is applied (will be set during dxf registration)
|
|
116
|
+
if hasattr(cf, '_dxf_id') and cf._dxf_id is not None:
|
|
117
|
+
attrs.append(f'dxfId="{cf._dxf_id}"')
|
|
118
|
+
|
|
119
|
+
attrs.append(f'priority="{cf.priority}"')
|
|
120
|
+
|
|
121
|
+
if cf.stop_if_true:
|
|
122
|
+
attrs.append('stopIfTrue="1"')
|
|
123
|
+
|
|
124
|
+
# Add operator for cellIs type
|
|
125
|
+
if rule_type == 'cellIs' and cf.operator:
|
|
126
|
+
attrs.append(f'operator="{cf.operator}"')
|
|
127
|
+
|
|
128
|
+
# Add text attribute for text-based rules
|
|
129
|
+
if rule_type in ('containsText', 'notContainsText', 'beginsWith', 'endsWith'):
|
|
130
|
+
if cf._formula1:
|
|
131
|
+
escaped_text = self._escape_xml(str(cf._formula1))
|
|
132
|
+
attrs.append(f'text="{escaped_text}"')
|
|
133
|
+
|
|
134
|
+
# Add timePeriod attribute
|
|
135
|
+
if rule_type == 'timePeriod' and cf._operator:
|
|
136
|
+
attrs.append(f'timePeriod="{cf._operator}"')
|
|
137
|
+
|
|
138
|
+
# Add top10 attributes
|
|
139
|
+
if rule_type == 'top10':
|
|
140
|
+
# Handle both 'top10' and 'bottom10' user types
|
|
141
|
+
# Both map to ECMA-376 'top10' type, distinguished by bottom attribute
|
|
142
|
+
if cf.top is not None:
|
|
143
|
+
attrs.append('bottom="0"' if cf.top else 'bottom="1"')
|
|
144
|
+
elif cf._type == 'bottom10':
|
|
145
|
+
# For bottom10 type, explicitly set bottom="1"
|
|
146
|
+
attrs.append('bottom="1"')
|
|
147
|
+
if cf.percent is not None:
|
|
148
|
+
attrs.append('percent="1"' if cf.percent else 'percent="0"')
|
|
149
|
+
if cf.rank is not None:
|
|
150
|
+
attrs.append(f'rank="{cf.rank}"')
|
|
151
|
+
|
|
152
|
+
# Add aboveAverage attributes
|
|
153
|
+
if rule_type == 'aboveAverage':
|
|
154
|
+
# Handle both 'aboveAverage' and 'belowAverage' user types
|
|
155
|
+
# Both map to ECMA-376 'aboveAverage' type, distinguished by aboveAverage attribute
|
|
156
|
+
if cf.above is not None:
|
|
157
|
+
attrs.append('aboveAverage="1"' if cf.above else 'aboveAverage="0"')
|
|
158
|
+
elif cf._type == 'belowAverage':
|
|
159
|
+
# For belowAverage type, explicitly set aboveAverage="0"
|
|
160
|
+
attrs.append('aboveAverage="0"')
|
|
161
|
+
if cf.std_dev is not None:
|
|
162
|
+
attrs.append(f'stdDev="{cf.std_dev}"')
|
|
163
|
+
|
|
164
|
+
xml = f' <cfRule {" ".join(attrs)}'
|
|
165
|
+
|
|
166
|
+
# Check if we need child elements
|
|
167
|
+
has_children = False
|
|
168
|
+
children_xml = ''
|
|
169
|
+
|
|
170
|
+
# Add formula elements based on rule type
|
|
171
|
+
if rule_type == 'expression':
|
|
172
|
+
# Expression (formula) rules use the _formula property
|
|
173
|
+
formula_value = cf._formula if cf._formula is not None else cf._formula1
|
|
174
|
+
if formula_value is not None:
|
|
175
|
+
formula_text = str(formula_value)
|
|
176
|
+
# Remove leading '=' if present
|
|
177
|
+
if formula_text.startswith('='):
|
|
178
|
+
formula_text = formula_text[1:]
|
|
179
|
+
escaped_formula = self._escape_xml(formula_text)
|
|
180
|
+
children_xml += f' <formula>{escaped_formula}</formula>\n'
|
|
181
|
+
has_children = True
|
|
182
|
+
elif rule_type == 'cellIs':
|
|
183
|
+
# Cell value rules use _formula1 and _formula2
|
|
184
|
+
if cf._formula1 is not None:
|
|
185
|
+
formula_text = str(cf._formula1)
|
|
186
|
+
if formula_text.startswith('='):
|
|
187
|
+
formula_text = formula_text[1:]
|
|
188
|
+
escaped_formula = self._escape_xml(formula_text)
|
|
189
|
+
children_xml += f' <formula>{escaped_formula}</formula>\n'
|
|
190
|
+
has_children = True
|
|
191
|
+
if cf._formula2 is not None:
|
|
192
|
+
formula_text = str(cf._formula2)
|
|
193
|
+
if formula_text.startswith('='):
|
|
194
|
+
formula_text = formula_text[1:]
|
|
195
|
+
escaped_formula = self._escape_xml(formula_text)
|
|
196
|
+
children_xml += f' <formula>{escaped_formula}</formula>\n'
|
|
197
|
+
has_children = True
|
|
198
|
+
elif rule_type in ('containsText', 'notContainsText', 'beginsWith', 'endsWith'):
|
|
199
|
+
# Text rules need a proper Excel formula
|
|
200
|
+
# Get the first cell of the range to build the formula
|
|
201
|
+
text_value = cf._formula1 if cf._formula1 is not None else ''
|
|
202
|
+
first_cell = self._get_first_cell_from_range(cf.range)
|
|
203
|
+
formula_text = self._build_text_rule_formula(rule_type, text_value, first_cell)
|
|
204
|
+
if formula_text:
|
|
205
|
+
escaped_formula = self._escape_xml(formula_text)
|
|
206
|
+
children_xml += f' <formula>{escaped_formula}</formula>\n'
|
|
207
|
+
has_children = True
|
|
208
|
+
|
|
209
|
+
# Add colorScale element
|
|
210
|
+
if rule_type == 'colorScale':
|
|
211
|
+
children_xml += self._format_color_scale_xml(cf)
|
|
212
|
+
has_children = True
|
|
213
|
+
|
|
214
|
+
# Add dataBar element
|
|
215
|
+
if rule_type == 'dataBar':
|
|
216
|
+
children_xml += self._format_data_bar_xml(cf)
|
|
217
|
+
has_children = True
|
|
218
|
+
|
|
219
|
+
# Add iconSet element
|
|
220
|
+
if rule_type == 'iconSet':
|
|
221
|
+
children_xml += self._format_icon_set_xml(cf)
|
|
222
|
+
has_children = True
|
|
223
|
+
|
|
224
|
+
if has_children:
|
|
225
|
+
xml += '>\n'
|
|
226
|
+
xml += children_xml
|
|
227
|
+
xml += ' </cfRule>\n'
|
|
228
|
+
else:
|
|
229
|
+
xml += '/>\n'
|
|
230
|
+
|
|
231
|
+
return xml
|
|
232
|
+
|
|
233
|
+
def _format_color_scale_xml(self, cf):
|
|
234
|
+
"""Formats a colorScale element for conditional formatting."""
|
|
235
|
+
xml = ' <colorScale>\n'
|
|
236
|
+
|
|
237
|
+
# Determine if 2-color or 3-color scale
|
|
238
|
+
is_3_color = cf.mid_color is not None or cf.color_scale_type == '3-color'
|
|
239
|
+
|
|
240
|
+
# Add cfvo elements (conditional format value objects)
|
|
241
|
+
xml += ' <cfvo type="min"/>\n'
|
|
242
|
+
if is_3_color:
|
|
243
|
+
xml += ' <cfvo type="percentile" val="50"/>\n'
|
|
244
|
+
xml += ' <cfvo type="max"/>\n'
|
|
245
|
+
|
|
246
|
+
# Add color elements
|
|
247
|
+
min_color = cf.min_color or 'FFF8696B' # Default red
|
|
248
|
+
max_color = cf.max_color or 'FF63BE7B' # Default green
|
|
249
|
+
mid_color = cf.mid_color or 'FFFFEB84' # Default yellow
|
|
250
|
+
|
|
251
|
+
xml += f' <color rgb="{min_color}"/>\n'
|
|
252
|
+
if is_3_color:
|
|
253
|
+
xml += f' <color rgb="{mid_color}"/>\n'
|
|
254
|
+
xml += f' <color rgb="{max_color}"/>\n'
|
|
255
|
+
|
|
256
|
+
xml += ' </colorScale>\n'
|
|
257
|
+
return xml
|
|
258
|
+
|
|
259
|
+
def _format_data_bar_xml(self, cf):
|
|
260
|
+
"""Formats a dataBar element for conditional formatting."""
|
|
261
|
+
xml = ' <dataBar>\n'
|
|
262
|
+
|
|
263
|
+
# Add cfvo elements
|
|
264
|
+
xml += ' <cfvo type="min"/>\n'
|
|
265
|
+
xml += ' <cfvo type="max"/>\n'
|
|
266
|
+
|
|
267
|
+
# Add color element
|
|
268
|
+
bar_color = cf.bar_color or 'FF638EC6' # Default blue
|
|
269
|
+
xml += f' <color rgb="{bar_color}"/>\n'
|
|
270
|
+
|
|
271
|
+
xml += ' </dataBar>\n'
|
|
272
|
+
return xml
|
|
273
|
+
|
|
274
|
+
def _format_icon_set_xml(self, cf):
|
|
275
|
+
"""Formats an iconSet element for conditional formatting."""
|
|
276
|
+
icon_set_type = cf.icon_set_type or '3TrafficLights1'
|
|
277
|
+
|
|
278
|
+
attrs = [f'iconSet="{icon_set_type}"']
|
|
279
|
+
if cf.reverse_icons:
|
|
280
|
+
attrs.append('reverse="1"')
|
|
281
|
+
if cf.show_icon_only:
|
|
282
|
+
attrs.append('showValue="0"')
|
|
283
|
+
|
|
284
|
+
xml = f' <iconSet {" ".join(attrs)}>\n'
|
|
285
|
+
|
|
286
|
+
# Add cfvo elements based on icon set type
|
|
287
|
+
# Determine number of icons
|
|
288
|
+
num_icons = 3
|
|
289
|
+
if icon_set_type.startswith('4'):
|
|
290
|
+
num_icons = 4
|
|
291
|
+
elif icon_set_type.startswith('5'):
|
|
292
|
+
num_icons = 5
|
|
293
|
+
|
|
294
|
+
# Add cfvo elements with percent thresholds
|
|
295
|
+
for i in range(num_icons):
|
|
296
|
+
percent_val = int(100 * i / num_icons)
|
|
297
|
+
xml += f' <cfvo type="percent" val="{percent_val}"/>\n'
|
|
298
|
+
|
|
299
|
+
xml += ' </iconSet>\n'
|
|
300
|
+
return xml
|
|
301
|
+
|
|
302
|
+
def _get_first_cell_from_range(self, cell_range):
|
|
303
|
+
"""
|
|
304
|
+
Gets the first cell reference from a range.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
cell_range (str): Cell range (e.g., "A1:A10" or "A1")
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
str: First cell reference (e.g., "A1")
|
|
311
|
+
"""
|
|
312
|
+
if not cell_range:
|
|
313
|
+
return 'A1'
|
|
314
|
+
if ':' in cell_range:
|
|
315
|
+
return cell_range.split(':')[0]
|
|
316
|
+
return cell_range
|
|
317
|
+
|
|
318
|
+
def _build_text_rule_formula(self, rule_type, text_value, first_cell):
|
|
319
|
+
"""
|
|
320
|
+
Builds the Excel formula for text-based conditional formatting rules.
|
|
321
|
+
|
|
322
|
+
According to ECMA-376, text rules require specific Excel formulas:
|
|
323
|
+
- containsText: NOT(ISERROR(SEARCH("text",A1)))
|
|
324
|
+
- notContainsText: ISERROR(SEARCH("text",A1))
|
|
325
|
+
- beginsWith: LEFT(A1,LEN("text"))="text"
|
|
326
|
+
- endsWith: RIGHT(A1,LEN("text"))="text"
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
rule_type (str): The rule type
|
|
330
|
+
text_value (str): The text to search for
|
|
331
|
+
first_cell (str): The first cell of the range
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
str: The Excel formula
|
|
335
|
+
"""
|
|
336
|
+
if not text_value:
|
|
337
|
+
return ''
|
|
338
|
+
|
|
339
|
+
# Escape double quotes in text value for Excel formula
|
|
340
|
+
escaped_text = str(text_value).replace('"', '""')
|
|
341
|
+
|
|
342
|
+
if rule_type == 'containsText':
|
|
343
|
+
return f'NOT(ISERROR(SEARCH("{escaped_text}",{first_cell})))'
|
|
344
|
+
elif rule_type == 'notContainsText':
|
|
345
|
+
return f'ISERROR(SEARCH("{escaped_text}",{first_cell}))'
|
|
346
|
+
elif rule_type == 'beginsWith':
|
|
347
|
+
return f'LEFT({first_cell},LEN("{escaped_text}"))="{escaped_text}"'
|
|
348
|
+
elif rule_type == 'endsWith':
|
|
349
|
+
return f'RIGHT({first_cell},LEN("{escaped_text}"))="{escaped_text}"'
|
|
350
|
+
|
|
351
|
+
return ''
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aspose.Cells for Python - Data Validation XML Loader
|
|
3
|
+
|
|
4
|
+
This module handles deserialization of DataValidation objects from ECMA-376 SpreadsheetML XML.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
- ECMA-376 Part 4, Section 3.3.1.30 (dataValidation)
|
|
8
|
+
- ECMA-376 Part 4, Section 3.3.1.31 (dataValidations)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import xml.etree.ElementTree as ET
|
|
12
|
+
from .data_validation import (
|
|
13
|
+
DataValidation, DataValidationCollection,
|
|
14
|
+
DataValidationType, DataValidationOperator,
|
|
15
|
+
DataValidationAlertStyle, DataValidationImeMode
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Mapping from XML attribute values to enum values
|
|
20
|
+
XML_TO_TYPE = {
|
|
21
|
+
'none': DataValidationType.NONE,
|
|
22
|
+
'whole': DataValidationType.WHOLE_NUMBER,
|
|
23
|
+
'decimal': DataValidationType.DECIMAL,
|
|
24
|
+
'list': DataValidationType.LIST,
|
|
25
|
+
'date': DataValidationType.DATE,
|
|
26
|
+
'time': DataValidationType.TIME,
|
|
27
|
+
'textLength': DataValidationType.TEXT_LENGTH,
|
|
28
|
+
'custom': DataValidationType.CUSTOM,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
XML_TO_OPERATOR = {
|
|
32
|
+
'between': DataValidationOperator.BETWEEN,
|
|
33
|
+
'notBetween': DataValidationOperator.NOT_BETWEEN,
|
|
34
|
+
'equal': DataValidationOperator.EQUAL,
|
|
35
|
+
'notEqual': DataValidationOperator.NOT_EQUAL,
|
|
36
|
+
'greaterThan': DataValidationOperator.GREATER_THAN,
|
|
37
|
+
'lessThan': DataValidationOperator.LESS_THAN,
|
|
38
|
+
'greaterThanOrEqual': DataValidationOperator.GREATER_THAN_OR_EQUAL,
|
|
39
|
+
'lessThanOrEqual': DataValidationOperator.LESS_THAN_OR_EQUAL,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
XML_TO_ALERT_STYLE = {
|
|
43
|
+
'stop': DataValidationAlertStyle.STOP,
|
|
44
|
+
'warning': DataValidationAlertStyle.WARNING,
|
|
45
|
+
'information': DataValidationAlertStyle.INFORMATION,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
XML_TO_IME_MODE = {
|
|
49
|
+
'noControl': DataValidationImeMode.NO_CONTROL,
|
|
50
|
+
'off': DataValidationImeMode.OFF,
|
|
51
|
+
'on': DataValidationImeMode.ON,
|
|
52
|
+
'disabled': DataValidationImeMode.DISABLED,
|
|
53
|
+
'hiragana': DataValidationImeMode.HIRAGANA,
|
|
54
|
+
'fullKatakana': DataValidationImeMode.FULL_KATAKANA,
|
|
55
|
+
'halfKatakana': DataValidationImeMode.HALF_KATAKANA,
|
|
56
|
+
'fullAlpha': DataValidationImeMode.FULL_ALPHA,
|
|
57
|
+
'halfAlpha': DataValidationImeMode.HALF_ALPHA,
|
|
58
|
+
'fullHangul': DataValidationImeMode.FULL_HANGUL,
|
|
59
|
+
'halfHangul': DataValidationImeMode.HALF_HANGUL,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DataValidationXmlLoader:
|
|
64
|
+
"""
|
|
65
|
+
Loads DataValidation objects from ECMA-376 SpreadsheetML XML format.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, namespace='http://schemas.openxmlformats.org/spreadsheetml/2006/main'):
|
|
69
|
+
"""
|
|
70
|
+
Initializes the DataValidationXmlLoader.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
namespace (str): The SpreadsheetML namespace URI.
|
|
74
|
+
"""
|
|
75
|
+
self.namespace = namespace
|
|
76
|
+
self.ns_prefix = '{' + namespace + '}'
|
|
77
|
+
|
|
78
|
+
def load_data_validations(self, parent_element):
|
|
79
|
+
"""
|
|
80
|
+
Loads a DataValidationCollection from a parent XML element.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
parent_element (Element): The parent XML element containing dataValidations.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
DataValidationCollection: The loaded validations.
|
|
87
|
+
"""
|
|
88
|
+
validations = DataValidationCollection()
|
|
89
|
+
|
|
90
|
+
# Find dataValidations element
|
|
91
|
+
dv_collection = parent_element.find(f'{self.ns_prefix}dataValidations')
|
|
92
|
+
|
|
93
|
+
if dv_collection is None:
|
|
94
|
+
# Try without namespace
|
|
95
|
+
dv_collection = parent_element.find('dataValidations')
|
|
96
|
+
|
|
97
|
+
if dv_collection is None:
|
|
98
|
+
return validations
|
|
99
|
+
|
|
100
|
+
# Load collection-level attributes
|
|
101
|
+
disable_prompts = dv_collection.get('disablePrompts', '0')
|
|
102
|
+
validations.disable_prompts = disable_prompts == '1' or disable_prompts.lower() == 'true'
|
|
103
|
+
|
|
104
|
+
x_window = dv_collection.get('xWindow')
|
|
105
|
+
if x_window is not None:
|
|
106
|
+
validations.x_window = int(x_window)
|
|
107
|
+
|
|
108
|
+
y_window = dv_collection.get('yWindow')
|
|
109
|
+
if y_window is not None:
|
|
110
|
+
validations.y_window = int(y_window)
|
|
111
|
+
|
|
112
|
+
# Load each dataValidation element
|
|
113
|
+
for dv_elem in dv_collection.findall(f'{self.ns_prefix}dataValidation'):
|
|
114
|
+
validation = self._load_data_validation(dv_elem)
|
|
115
|
+
validations.add_validation(validation)
|
|
116
|
+
|
|
117
|
+
# Try without namespace if none found
|
|
118
|
+
if validations.count == 0:
|
|
119
|
+
for dv_elem in dv_collection.findall('dataValidation'):
|
|
120
|
+
validation = self._load_data_validation(dv_elem)
|
|
121
|
+
validations.add_validation(validation)
|
|
122
|
+
|
|
123
|
+
return validations
|
|
124
|
+
|
|
125
|
+
def _load_data_validation(self, dv_elem):
|
|
126
|
+
"""
|
|
127
|
+
Loads a single DataValidation from an XML element.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
dv_elem (Element): The dataValidation XML element.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
DataValidation: The loaded validation.
|
|
134
|
+
"""
|
|
135
|
+
# Get sqref (required)
|
|
136
|
+
sqref = dv_elem.get('sqref', '')
|
|
137
|
+
validation = DataValidation(sqref)
|
|
138
|
+
|
|
139
|
+
# Load type
|
|
140
|
+
type_str = dv_elem.get('type', 'none')
|
|
141
|
+
validation.type = XML_TO_TYPE.get(type_str, DataValidationType.NONE)
|
|
142
|
+
|
|
143
|
+
# Load operator
|
|
144
|
+
operator_str = dv_elem.get('operator', 'between')
|
|
145
|
+
validation.operator = XML_TO_OPERATOR.get(operator_str, DataValidationOperator.BETWEEN)
|
|
146
|
+
|
|
147
|
+
# Load error style
|
|
148
|
+
error_style_str = dv_elem.get('errorStyle', 'stop')
|
|
149
|
+
validation.alert_style = XML_TO_ALERT_STYLE.get(error_style_str, DataValidationAlertStyle.STOP)
|
|
150
|
+
|
|
151
|
+
# Load IME mode
|
|
152
|
+
ime_mode_str = dv_elem.get('imeMode', 'noControl')
|
|
153
|
+
validation.ime_mode = XML_TO_IME_MODE.get(ime_mode_str, DataValidationImeMode.NO_CONTROL)
|
|
154
|
+
|
|
155
|
+
# Load boolean attributes
|
|
156
|
+
allow_blank = dv_elem.get('allowBlank', '0')
|
|
157
|
+
validation.allow_blank = allow_blank == '1' or allow_blank.lower() == 'true'
|
|
158
|
+
|
|
159
|
+
# Note: In ECMA-376, showDropDown="1" means HIDE the dropdown (counterintuitive)
|
|
160
|
+
show_dropdown = dv_elem.get('showDropDown', '0')
|
|
161
|
+
validation.show_dropdown = not (show_dropdown == '1' or show_dropdown.lower() == 'true')
|
|
162
|
+
|
|
163
|
+
show_input = dv_elem.get('showInputMessage', '0')
|
|
164
|
+
validation.show_input_message = show_input == '1' or show_input.lower() == 'true'
|
|
165
|
+
|
|
166
|
+
show_error = dv_elem.get('showErrorMessage', '0')
|
|
167
|
+
validation.show_error_message = show_error == '1' or show_error.lower() == 'true'
|
|
168
|
+
|
|
169
|
+
# Load string attributes
|
|
170
|
+
validation.error_title = dv_elem.get('errorTitle')
|
|
171
|
+
validation.error_message = dv_elem.get('error')
|
|
172
|
+
validation.input_title = dv_elem.get('promptTitle')
|
|
173
|
+
validation.input_message = dv_elem.get('prompt')
|
|
174
|
+
|
|
175
|
+
# Load formula elements
|
|
176
|
+
formula1_elem = dv_elem.find(f'{self.ns_prefix}formula1')
|
|
177
|
+
if formula1_elem is None:
|
|
178
|
+
formula1_elem = dv_elem.find('formula1')
|
|
179
|
+
if formula1_elem is not None and formula1_elem.text:
|
|
180
|
+
validation.formula1 = formula1_elem.text
|
|
181
|
+
|
|
182
|
+
formula2_elem = dv_elem.find(f'{self.ns_prefix}formula2')
|
|
183
|
+
if formula2_elem is None:
|
|
184
|
+
formula2_elem = dv_elem.find('formula2')
|
|
185
|
+
if formula2_elem is not None and formula2_elem.text:
|
|
186
|
+
validation.formula2 = formula2_elem.text
|
|
187
|
+
|
|
188
|
+
return validation
|
|
189
|
+
|
|
190
|
+
def load_from_xml_string(self, xml_string):
|
|
191
|
+
"""
|
|
192
|
+
Loads validations from an XML string.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
xml_string (str): The XML string containing dataValidations.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
DataValidationCollection: The loaded validations.
|
|
199
|
+
"""
|
|
200
|
+
# Parse the XML
|
|
201
|
+
root = ET.fromstring(xml_string)
|
|
202
|
+
|
|
203
|
+
# Check if root is dataValidations
|
|
204
|
+
if root.tag.endswith('dataValidations'):
|
|
205
|
+
# Create a temporary parent
|
|
206
|
+
temp_parent = ET.Element('temp')
|
|
207
|
+
temp_parent.append(root)
|
|
208
|
+
return self.load_data_validations(temp_parent)
|
|
209
|
+
|
|
210
|
+
return self.load_data_validations(root)
|
|
211
|
+
|
|
212
|
+
def load_from_file(self, file_path):
|
|
213
|
+
"""
|
|
214
|
+
Loads validations from an XML file.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
file_path (str): Path to the XML file.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
DataValidationCollection: The loaded validations.
|
|
221
|
+
"""
|
|
222
|
+
tree = ET.parse(file_path)
|
|
223
|
+
root = tree.getroot()
|
|
224
|
+
return self.load_data_validations(root)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def load_data_validations_from_worksheet_xml(worksheet_element, namespace):
|
|
228
|
+
"""
|
|
229
|
+
Convenience function to load data validations from a worksheet XML element.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
worksheet_element (Element): The worksheet XML element.
|
|
233
|
+
namespace (str): The SpreadsheetML namespace.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
DataValidationCollection: The loaded validations.
|
|
237
|
+
"""
|
|
238
|
+
loader = DataValidationXmlLoader(namespace)
|
|
239
|
+
return loader.load_data_validations(worksheet_element)
|