aspose-cells-foss 25.12.1__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/__init__.py +14 -0
- aspose/cells/__init__.py +31 -0
- aspose/cells/cell.py +350 -0
- aspose/cells/constants.py +44 -0
- aspose/cells/converters/__init__.py +13 -0
- aspose/cells/converters/csv_converter.py +55 -0
- aspose/cells/converters/json_converter.py +46 -0
- aspose/cells/converters/markdown_converter.py +453 -0
- aspose/cells/drawing/__init__.py +17 -0
- aspose/cells/drawing/anchor.py +172 -0
- aspose/cells/drawing/collection.py +233 -0
- aspose/cells/drawing/image.py +338 -0
- aspose/cells/formats.py +80 -0
- aspose/cells/formula/__init__.py +10 -0
- aspose/cells/formula/evaluator.py +360 -0
- aspose/cells/formula/functions.py +433 -0
- aspose/cells/formula/tokenizer.py +340 -0
- aspose/cells/io/__init__.py +27 -0
- aspose/cells/io/csv/__init__.py +8 -0
- aspose/cells/io/csv/reader.py +88 -0
- aspose/cells/io/csv/writer.py +98 -0
- aspose/cells/io/factory.py +138 -0
- aspose/cells/io/interfaces.py +48 -0
- aspose/cells/io/json/__init__.py +8 -0
- aspose/cells/io/json/reader.py +126 -0
- aspose/cells/io/json/writer.py +119 -0
- aspose/cells/io/md/__init__.py +8 -0
- aspose/cells/io/md/reader.py +161 -0
- aspose/cells/io/md/writer.py +334 -0
- aspose/cells/io/models.py +64 -0
- aspose/cells/io/xlsx/__init__.py +9 -0
- aspose/cells/io/xlsx/constants.py +312 -0
- aspose/cells/io/xlsx/image_writer.py +311 -0
- aspose/cells/io/xlsx/reader.py +284 -0
- aspose/cells/io/xlsx/writer.py +931 -0
- aspose/cells/plugins/__init__.py +6 -0
- aspose/cells/plugins/docling_backend/__init__.py +7 -0
- aspose/cells/plugins/docling_backend/backend.py +535 -0
- aspose/cells/plugins/markitdown_plugin/__init__.py +15 -0
- aspose/cells/plugins/markitdown_plugin/plugin.py +128 -0
- aspose/cells/range.py +210 -0
- aspose/cells/style.py +287 -0
- aspose/cells/utils/__init__.py +54 -0
- aspose/cells/utils/coordinates.py +68 -0
- aspose/cells/utils/exceptions.py +43 -0
- aspose/cells/utils/validation.py +102 -0
- aspose/cells/workbook.py +352 -0
- aspose/cells/worksheet.py +670 -0
- aspose_cells_foss-25.12.1.dist-info/METADATA +189 -0
- aspose_cells_foss-25.12.1.dist-info/RECORD +53 -0
- aspose_cells_foss-25.12.1.dist-info/WHEEL +5 -0
- aspose_cells_foss-25.12.1.dist-info/entry_points.txt +2 -0
- aspose_cells_foss-25.12.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Excel Formula Tokenizer - Based on opencells tokenizer
|
|
3
|
+
Converts Excel formulas into token streams for evaluation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Token:
|
|
11
|
+
"""Represents a single token in a formula."""
|
|
12
|
+
|
|
13
|
+
# Token types
|
|
14
|
+
LITERAL = "LITERAL"
|
|
15
|
+
OPERAND = "OPERAND"
|
|
16
|
+
FUNCTION = "FUNCTION"
|
|
17
|
+
SUBEXPR = "SUBEXPR"
|
|
18
|
+
ARGUMENT = "ARGUMENT"
|
|
19
|
+
OPERATOR = "OPERATOR"
|
|
20
|
+
WHITESPACE = "WHITESPACE"
|
|
21
|
+
ERROR = "ERROR"
|
|
22
|
+
|
|
23
|
+
# Token subtypes
|
|
24
|
+
TEXT = "TEXT"
|
|
25
|
+
NUMBER = "NUMBER"
|
|
26
|
+
LOGICAL = "LOGICAL"
|
|
27
|
+
RANGE = "RANGE"
|
|
28
|
+
REFERENCE = "REFERENCE"
|
|
29
|
+
NAME = "NAME"
|
|
30
|
+
|
|
31
|
+
# Operator types
|
|
32
|
+
MATH = "MATH"
|
|
33
|
+
CONCAT = "CONCAT"
|
|
34
|
+
INTERSECT = "INTERSECT"
|
|
35
|
+
UNION = "UNION"
|
|
36
|
+
|
|
37
|
+
def __init__(self, value: str, type_: str, subtype: str = ""):
|
|
38
|
+
self.value = value
|
|
39
|
+
self.type = type_
|
|
40
|
+
self.subtype = subtype
|
|
41
|
+
|
|
42
|
+
def __repr__(self):
|
|
43
|
+
return f"Token({self.value!r}, {self.type}, {self.subtype})"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Tokenizer:
|
|
47
|
+
"""Tokenizer for Excel formulas."""
|
|
48
|
+
|
|
49
|
+
# Regex patterns
|
|
50
|
+
CELL_REF_PATTERN = re.compile(r'^(\$?)([A-Z]+)(\$?)(\d+)$')
|
|
51
|
+
RANGE_PATTERN = re.compile(r'^(\$?[A-Z]+\$?\d+):(\$?[A-Z]+\$?\d+)$')
|
|
52
|
+
FUNCTION_PATTERN = re.compile(r'^[A-Z_][A-Z0-9_.]*$')
|
|
53
|
+
NUMBER_PATTERN = re.compile(r'^-?\d+(\.\d*)?([Ee][+-]?\d+)?$|^-?\d*\.\d+([Ee][+-]?\d+)?$')
|
|
54
|
+
|
|
55
|
+
# Excel error codes
|
|
56
|
+
ERROR_CODES = {'#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!', '#N/A'}
|
|
57
|
+
|
|
58
|
+
def __init__(self, formula: str):
|
|
59
|
+
self.formula = formula.strip()
|
|
60
|
+
self.tokens: List[Token] = []
|
|
61
|
+
self.position = 0
|
|
62
|
+
self._tokenize()
|
|
63
|
+
|
|
64
|
+
def _tokenize(self):
|
|
65
|
+
"""Parse the formula into tokens."""
|
|
66
|
+
if not self.formula:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# Skip leading = if present
|
|
70
|
+
if self.formula.startswith('='):
|
|
71
|
+
self.position = 1
|
|
72
|
+
|
|
73
|
+
while self.position < len(self.formula):
|
|
74
|
+
self._skip_whitespace()
|
|
75
|
+
if self.position >= len(self.formula):
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
if self._try_string():
|
|
79
|
+
continue
|
|
80
|
+
elif self._try_number():
|
|
81
|
+
continue
|
|
82
|
+
elif self._try_operator():
|
|
83
|
+
continue
|
|
84
|
+
elif self._try_function():
|
|
85
|
+
continue
|
|
86
|
+
elif self._try_reference():
|
|
87
|
+
continue
|
|
88
|
+
elif self._try_error():
|
|
89
|
+
continue
|
|
90
|
+
elif self._try_parenthesis():
|
|
91
|
+
continue
|
|
92
|
+
elif self._try_separator():
|
|
93
|
+
continue
|
|
94
|
+
else:
|
|
95
|
+
# Unknown character, treat as text
|
|
96
|
+
self._consume_text()
|
|
97
|
+
|
|
98
|
+
def _current_char(self) -> Optional[str]:
|
|
99
|
+
"""Get current character."""
|
|
100
|
+
if self.position < len(self.formula):
|
|
101
|
+
return self.formula[self.position]
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def _peek_char(self, offset: int = 1) -> Optional[str]:
|
|
105
|
+
"""Peek ahead at character."""
|
|
106
|
+
pos = self.position + offset
|
|
107
|
+
if pos < len(self.formula):
|
|
108
|
+
return self.formula[pos]
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def _skip_whitespace(self):
|
|
112
|
+
"""Skip whitespace characters."""
|
|
113
|
+
while self.position < len(self.formula) and self.formula[self.position].isspace():
|
|
114
|
+
self.position += 1
|
|
115
|
+
|
|
116
|
+
def _try_string(self) -> bool:
|
|
117
|
+
"""Try to parse a quoted string."""
|
|
118
|
+
char = self._current_char()
|
|
119
|
+
if char not in ('"', "'"):
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
quote_char = char
|
|
123
|
+
start_pos = self.position
|
|
124
|
+
self.position += 1 # Skip opening quote
|
|
125
|
+
value = ''
|
|
126
|
+
|
|
127
|
+
while self.position < len(self.formula):
|
|
128
|
+
char = self._current_char()
|
|
129
|
+
if char == quote_char:
|
|
130
|
+
# Check for escaped quote (doubled quotes)
|
|
131
|
+
if self._peek_char() == quote_char:
|
|
132
|
+
value += quote_char
|
|
133
|
+
self.position += 2
|
|
134
|
+
else:
|
|
135
|
+
self.position += 1 # Skip closing quote
|
|
136
|
+
break
|
|
137
|
+
else:
|
|
138
|
+
value += char
|
|
139
|
+
self.position += 1
|
|
140
|
+
|
|
141
|
+
self.tokens.append(Token(value, Token.OPERAND, Token.TEXT))
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
def _try_number(self) -> bool:
|
|
145
|
+
"""Try to parse a number."""
|
|
146
|
+
start_pos = self.position
|
|
147
|
+
value = ''
|
|
148
|
+
|
|
149
|
+
# Only handle negative sign if it's at the start or after an operator/opening paren
|
|
150
|
+
can_be_negative = (
|
|
151
|
+
len(self.tokens) == 0 or # Start of formula
|
|
152
|
+
self.tokens[-1].type in (Token.OPERATOR, Token.SUBEXPR, Token.ARGUMENT)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Handle negative sign only in valid contexts
|
|
156
|
+
if self._current_char() == '-' and can_be_negative:
|
|
157
|
+
value += '-'
|
|
158
|
+
self.position += 1
|
|
159
|
+
elif self._current_char() == '-':
|
|
160
|
+
# It's likely a subtraction operator, not a negative number
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
# Collect digits and decimal points
|
|
164
|
+
while self.position < len(self.formula):
|
|
165
|
+
char = self._current_char()
|
|
166
|
+
if char.isdigit() or char == '.':
|
|
167
|
+
value += char
|
|
168
|
+
self.position += 1
|
|
169
|
+
elif char in 'Ee' and value and value[-1].isdigit():
|
|
170
|
+
# Scientific notation
|
|
171
|
+
value += char
|
|
172
|
+
self.position += 1
|
|
173
|
+
# Handle optional +/- after E
|
|
174
|
+
if self._current_char() in '+-':
|
|
175
|
+
value += self._current_char()
|
|
176
|
+
self.position += 1
|
|
177
|
+
else:
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
if value and self.NUMBER_PATTERN.match(value):
|
|
181
|
+
self.tokens.append(Token(value, Token.OPERAND, Token.NUMBER))
|
|
182
|
+
return True
|
|
183
|
+
else:
|
|
184
|
+
# Not a valid number, reset position
|
|
185
|
+
self.position = start_pos
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def _try_operator(self) -> bool:
|
|
189
|
+
"""Try to parse an operator."""
|
|
190
|
+
char = self._current_char()
|
|
191
|
+
operators = {
|
|
192
|
+
'+': (Token.OPERATOR, Token.MATH),
|
|
193
|
+
'-': (Token.OPERATOR, Token.MATH),
|
|
194
|
+
'*': (Token.OPERATOR, Token.MATH),
|
|
195
|
+
'/': (Token.OPERATOR, Token.MATH),
|
|
196
|
+
'^': (Token.OPERATOR, Token.MATH),
|
|
197
|
+
'&': (Token.OPERATOR, Token.CONCAT),
|
|
198
|
+
'=': (Token.OPERATOR, Token.MATH),
|
|
199
|
+
'<': (Token.OPERATOR, Token.MATH),
|
|
200
|
+
'>': (Token.OPERATOR, Token.MATH),
|
|
201
|
+
'%': (Token.OPERATOR, Token.MATH),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if char in operators:
|
|
205
|
+
# Check for multi-character operators
|
|
206
|
+
next_char = self._peek_char()
|
|
207
|
+
if char == '<' and next_char == '>':
|
|
208
|
+
self.tokens.append(Token('<>', Token.OPERATOR, Token.MATH))
|
|
209
|
+
self.position += 2
|
|
210
|
+
elif char == '<' and next_char == '=':
|
|
211
|
+
self.tokens.append(Token('<=', Token.OPERATOR, Token.MATH))
|
|
212
|
+
self.position += 2
|
|
213
|
+
elif char == '>' and next_char == '=':
|
|
214
|
+
self.tokens.append(Token('>=', Token.OPERATOR, Token.MATH))
|
|
215
|
+
self.position += 2
|
|
216
|
+
else:
|
|
217
|
+
type_, subtype = operators[char]
|
|
218
|
+
self.tokens.append(Token(char, type_, subtype))
|
|
219
|
+
self.position += 1
|
|
220
|
+
return True
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def _try_function(self) -> bool:
|
|
224
|
+
"""Try to parse a function name."""
|
|
225
|
+
start_pos = self.position
|
|
226
|
+
value = ''
|
|
227
|
+
|
|
228
|
+
# Functions start with letter or underscore
|
|
229
|
+
char = self._current_char()
|
|
230
|
+
if not (char.isalpha() or char == '_'):
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
# Collect function name
|
|
234
|
+
while self.position < len(self.formula):
|
|
235
|
+
char = self._current_char()
|
|
236
|
+
if char.isalnum() or char in '_.':
|
|
237
|
+
value += char
|
|
238
|
+
self.position += 1
|
|
239
|
+
else:
|
|
240
|
+
break
|
|
241
|
+
|
|
242
|
+
# Check if followed by opening parenthesis
|
|
243
|
+
self._skip_whitespace()
|
|
244
|
+
if self._current_char() == '(':
|
|
245
|
+
if self.FUNCTION_PATTERN.match(value.upper()):
|
|
246
|
+
self.tokens.append(Token(value.upper(), Token.FUNCTION))
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
# Not a function, reset position
|
|
250
|
+
self.position = start_pos
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
def _try_reference(self) -> bool:
|
|
254
|
+
"""Try to parse a cell reference or range."""
|
|
255
|
+
start_pos = self.position
|
|
256
|
+
value = ''
|
|
257
|
+
|
|
258
|
+
# Collect potential reference
|
|
259
|
+
while self.position < len(self.formula):
|
|
260
|
+
char = self._current_char()
|
|
261
|
+
if char.isalnum() or char in '$:':
|
|
262
|
+
value += char
|
|
263
|
+
self.position += 1
|
|
264
|
+
else:
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
if value:
|
|
268
|
+
# Check for range (contains colon)
|
|
269
|
+
if ':' in value and self.RANGE_PATTERN.match(value.upper()):
|
|
270
|
+
self.tokens.append(Token(value.upper(), Token.OPERAND, Token.RANGE))
|
|
271
|
+
return True
|
|
272
|
+
# Check for single cell reference
|
|
273
|
+
elif self.CELL_REF_PATTERN.match(value.upper()):
|
|
274
|
+
self.tokens.append(Token(value.upper(), Token.OPERAND, Token.REFERENCE))
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
# Not a reference, reset position
|
|
278
|
+
self.position = start_pos
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
def _try_error(self) -> bool:
|
|
282
|
+
"""Try to parse an error value."""
|
|
283
|
+
start_pos = self.position
|
|
284
|
+
if self._current_char() != '#':
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
value = ''
|
|
288
|
+
while self.position < len(self.formula):
|
|
289
|
+
char = self._current_char()
|
|
290
|
+
if char.isalnum() or char in '#!/?':
|
|
291
|
+
value += char
|
|
292
|
+
self.position += 1
|
|
293
|
+
else:
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
if value in self.ERROR_CODES:
|
|
297
|
+
self.tokens.append(Token(value, Token.OPERAND, Token.ERROR))
|
|
298
|
+
return True
|
|
299
|
+
else:
|
|
300
|
+
self.position = start_pos
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
def _try_parenthesis(self) -> bool:
|
|
304
|
+
"""Try to parse parentheses."""
|
|
305
|
+
char = self._current_char()
|
|
306
|
+
if char == '(':
|
|
307
|
+
self.tokens.append(Token('(', Token.SUBEXPR, "OPEN"))
|
|
308
|
+
self.position += 1
|
|
309
|
+
return True
|
|
310
|
+
elif char == ')':
|
|
311
|
+
self.tokens.append(Token(')', Token.SUBEXPR, "CLOSE"))
|
|
312
|
+
self.position += 1
|
|
313
|
+
return True
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
def _try_separator(self) -> bool:
|
|
317
|
+
"""Try to parse separators (comma, semicolon)."""
|
|
318
|
+
char = self._current_char()
|
|
319
|
+
if char in (',', ';'):
|
|
320
|
+
self.tokens.append(Token(char, Token.ARGUMENT))
|
|
321
|
+
self.position += 1
|
|
322
|
+
return True
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
def _consume_text(self):
|
|
326
|
+
"""Consume remaining text as literal."""
|
|
327
|
+
value = ''
|
|
328
|
+
while self.position < len(self.formula):
|
|
329
|
+
char = self._current_char()
|
|
330
|
+
if char.isspace() or char in '()+-*/^&=<>%,;':
|
|
331
|
+
break
|
|
332
|
+
value += char
|
|
333
|
+
self.position += 1
|
|
334
|
+
|
|
335
|
+
if value:
|
|
336
|
+
self.tokens.append(Token(value, Token.OPERAND, Token.TEXT))
|
|
337
|
+
|
|
338
|
+
def __iter__(self):
|
|
339
|
+
"""Iterate over tokens."""
|
|
340
|
+
return iter(self.tokens)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
I/O module for Excel file reading and writing with unified format support.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Format-specific readers and writers
|
|
6
|
+
from .csv import CsvReader, CsvWriter
|
|
7
|
+
from .json import JsonReader, JsonWriter
|
|
8
|
+
from .md import MarkdownReader, MarkdownWriter
|
|
9
|
+
from .xlsx import XlsxReader, XlsxWriter
|
|
10
|
+
|
|
11
|
+
# Unified architecture components
|
|
12
|
+
from .models import WorkbookData
|
|
13
|
+
from .interfaces import IFormatHandler
|
|
14
|
+
from .factory import FormatHandlerFactory
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
# Format-specific components
|
|
18
|
+
"CsvReader", "CsvWriter",
|
|
19
|
+
"JsonReader", "JsonWriter",
|
|
20
|
+
"MarkdownReader", "MarkdownWriter",
|
|
21
|
+
"XlsxReader", "XlsxWriter",
|
|
22
|
+
|
|
23
|
+
# Unified architecture components
|
|
24
|
+
"WorkbookData",
|
|
25
|
+
"IFormatHandler",
|
|
26
|
+
"FormatHandlerFactory"
|
|
27
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CSV file reader for loading CSV data into workbook format.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from typing import Dict, List, Optional, Union, TYPE_CHECKING
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from ...formats import CellValue
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ...workbook import Workbook
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CsvReader:
|
|
15
|
+
"""Reader for CSV files."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def read(self, file_path: str, **kwargs) -> List[List[CellValue]]:
|
|
21
|
+
"""Read CSV file and return data as list of rows."""
|
|
22
|
+
delimiter = kwargs.get('delimiter', ',')
|
|
23
|
+
quotechar = kwargs.get('quotechar', '"')
|
|
24
|
+
encoding = kwargs.get('encoding', 'utf-8')
|
|
25
|
+
has_header = kwargs.get('has_header', False)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
with open(file_path, 'r', encoding=encoding, newline='') as file:
|
|
29
|
+
reader = csv.reader(file, delimiter=delimiter, quotechar=quotechar)
|
|
30
|
+
|
|
31
|
+
data = []
|
|
32
|
+
for row in reader:
|
|
33
|
+
# Convert each cell value
|
|
34
|
+
converted_row = []
|
|
35
|
+
for cell in row:
|
|
36
|
+
converted_row.append(self._convert_cell_value(cell))
|
|
37
|
+
data.append(converted_row)
|
|
38
|
+
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
except FileNotFoundError:
|
|
42
|
+
raise FileNotFoundError(f"CSV file not found: {file_path}")
|
|
43
|
+
except Exception as e:
|
|
44
|
+
raise ValueError(f"Error reading CSV file: {e}")
|
|
45
|
+
|
|
46
|
+
def _convert_cell_value(self, value: str) -> CellValue:
|
|
47
|
+
"""Convert string value to appropriate Python type."""
|
|
48
|
+
if not value or value.strip() == "":
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
value = value.strip()
|
|
52
|
+
|
|
53
|
+
# Try boolean first
|
|
54
|
+
if value.upper() in ('TRUE', 'FALSE'):
|
|
55
|
+
return value.upper() == 'TRUE'
|
|
56
|
+
|
|
57
|
+
# Try integer
|
|
58
|
+
try:
|
|
59
|
+
if '.' not in value and 'e' not in value.lower():
|
|
60
|
+
return int(value)
|
|
61
|
+
except ValueError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# Try float
|
|
65
|
+
try:
|
|
66
|
+
return float(value)
|
|
67
|
+
except ValueError:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
# Return as string
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
def load_workbook(self, workbook: 'Workbook', file_path: str, **options) -> None:
|
|
74
|
+
"""Load CSV file into workbook object."""
|
|
75
|
+
data = self.read(file_path, **options)
|
|
76
|
+
|
|
77
|
+
# Clear existing worksheets
|
|
78
|
+
workbook._worksheets.clear()
|
|
79
|
+
workbook._active_sheet = None
|
|
80
|
+
|
|
81
|
+
# Create single worksheet
|
|
82
|
+
worksheet = workbook.create_sheet("Sheet1")
|
|
83
|
+
|
|
84
|
+
# Populate worksheet with CSV data
|
|
85
|
+
for row_idx, row_data in enumerate(data, 1):
|
|
86
|
+
for col_idx, cell_value in enumerate(row_data, 1):
|
|
87
|
+
if cell_value is not None:
|
|
88
|
+
worksheet.cell(row_idx, col_idx, cell_value)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CSV file writer for saving workbook data to CSV format.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import io
|
|
7
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
8
|
+
from ...formats import CellValue
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ...workbook import Workbook
|
|
12
|
+
from ...worksheet import Worksheet
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CsvWriter:
|
|
16
|
+
"""Writer for CSV files."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def write(self, file_path: str, data: List[List[CellValue]], **kwargs) -> None:
|
|
22
|
+
"""Write data to CSV file."""
|
|
23
|
+
delimiter = kwargs.get('delimiter', ',')
|
|
24
|
+
quotechar = kwargs.get('quotechar', '"')
|
|
25
|
+
encoding = kwargs.get('encoding', 'utf-8')
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
with open(file_path, 'w', newline='', encoding=encoding) as file:
|
|
29
|
+
writer = csv.writer(file, delimiter=delimiter, quotechar=quotechar,
|
|
30
|
+
quoting=csv.QUOTE_MINIMAL)
|
|
31
|
+
|
|
32
|
+
for row in data:
|
|
33
|
+
formatted_row = []
|
|
34
|
+
for cell in row:
|
|
35
|
+
formatted_row.append(self._format_cell_value(cell))
|
|
36
|
+
writer.writerow(formatted_row)
|
|
37
|
+
|
|
38
|
+
except Exception as e:
|
|
39
|
+
raise ValueError(f"Error writing CSV file: {e}")
|
|
40
|
+
|
|
41
|
+
def write_workbook(self, file_path: str, workbook: 'Workbook', **kwargs) -> None:
|
|
42
|
+
"""Write workbook data to CSV file."""
|
|
43
|
+
sheet_name = kwargs.get('sheet_name')
|
|
44
|
+
|
|
45
|
+
# Get target worksheet
|
|
46
|
+
if sheet_name and sheet_name in workbook._worksheets:
|
|
47
|
+
worksheet = workbook._worksheets[sheet_name]
|
|
48
|
+
else:
|
|
49
|
+
worksheet = workbook.active
|
|
50
|
+
|
|
51
|
+
if not worksheet or not worksheet._cells:
|
|
52
|
+
# Write empty file
|
|
53
|
+
with open(file_path, 'w', newline='', encoding=kwargs.get('encoding', 'utf-8')) as file:
|
|
54
|
+
pass
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Convert worksheet to data
|
|
58
|
+
data = self._worksheet_to_data(worksheet)
|
|
59
|
+
self.write(file_path, data, **kwargs)
|
|
60
|
+
|
|
61
|
+
def _worksheet_to_data(self, worksheet: 'Worksheet') -> List[List[CellValue]]:
|
|
62
|
+
"""Convert worksheet to list of rows."""
|
|
63
|
+
max_row = worksheet.max_row
|
|
64
|
+
max_col = worksheet.max_column
|
|
65
|
+
|
|
66
|
+
if max_row == 0 or max_col == 0:
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
data = []
|
|
70
|
+
for row in range(1, max_row + 1):
|
|
71
|
+
row_data = []
|
|
72
|
+
for col in range(1, max_col + 1):
|
|
73
|
+
cell = worksheet._cells.get((row, col))
|
|
74
|
+
if cell and cell.value is not None:
|
|
75
|
+
row_data.append(cell.value)
|
|
76
|
+
else:
|
|
77
|
+
row_data.append(None)
|
|
78
|
+
|
|
79
|
+
# Skip completely empty rows unless they're in the middle
|
|
80
|
+
if any(val is not None for val in row_data) or row < max_row:
|
|
81
|
+
data.append(row_data)
|
|
82
|
+
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
def _format_cell_value(self, value: CellValue) -> str:
|
|
86
|
+
"""Format cell value for CSV output."""
|
|
87
|
+
if value is None:
|
|
88
|
+
return ""
|
|
89
|
+
elif isinstance(value, bool):
|
|
90
|
+
return "TRUE" if value else "FALSE"
|
|
91
|
+
elif isinstance(value, (int, float)):
|
|
92
|
+
return str(value)
|
|
93
|
+
else:
|
|
94
|
+
return str(value)
|
|
95
|
+
|
|
96
|
+
def save_workbook(self, workbook: 'Workbook', file_path: str, **options) -> None:
|
|
97
|
+
"""Save workbook to CSV file - unified interface method."""
|
|
98
|
+
self.write_workbook(file_path, workbook, **options)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Format handler factory for unified file processing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Optional, Type, List
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from .interfaces import IFormatHandler
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FormatHandlerFactory:
|
|
11
|
+
"""Factory for managing format handlers."""
|
|
12
|
+
|
|
13
|
+
_handlers: Dict[str, Type[IFormatHandler]] = {}
|
|
14
|
+
_instances: Dict[str, IFormatHandler] = {}
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def register(cls, extension: str, handler_class: Type[IFormatHandler]) -> None:
|
|
18
|
+
"""Register format handler for file extension."""
|
|
19
|
+
if not extension.startswith('.'):
|
|
20
|
+
extension = '.' + extension
|
|
21
|
+
cls._handlers[extension.lower()] = handler_class
|
|
22
|
+
# Clear instance cache when registering new handler
|
|
23
|
+
if extension.lower() in cls._instances:
|
|
24
|
+
del cls._instances[extension.lower()]
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def get_handler(cls, file_path: str) -> Optional[IFormatHandler]:
|
|
28
|
+
"""Get format handler for file extension."""
|
|
29
|
+
ext = Path(file_path).suffix.lower()
|
|
30
|
+
|
|
31
|
+
if ext not in cls._handlers:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
# Use singleton pattern for handler instances
|
|
35
|
+
if ext not in cls._instances:
|
|
36
|
+
cls._instances[ext] = cls._handlers[ext]()
|
|
37
|
+
|
|
38
|
+
return cls._instances[ext]
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def get_supported_formats(cls) -> List[str]:
|
|
42
|
+
"""Get list of supported file extensions."""
|
|
43
|
+
return list(cls._handlers.keys())
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def is_supported(cls, file_path: str) -> bool:
|
|
47
|
+
"""Check if file format is supported."""
|
|
48
|
+
ext = Path(file_path).suffix.lower()
|
|
49
|
+
return ext in cls._handlers
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def clear_cache(cls) -> None:
|
|
53
|
+
"""Clear handler instance cache."""
|
|
54
|
+
cls._instances.clear()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _register_builtin_formats():
|
|
58
|
+
"""Register built-in format handlers."""
|
|
59
|
+
|
|
60
|
+
# XLSX Handler
|
|
61
|
+
class XlsxHandler(IFormatHandler):
|
|
62
|
+
"""Handler for XLSX format."""
|
|
63
|
+
|
|
64
|
+
def __init__(self):
|
|
65
|
+
from .xlsx.reader import XlsxReader
|
|
66
|
+
from .xlsx.writer import XlsxWriter
|
|
67
|
+
self._reader = XlsxReader()
|
|
68
|
+
self._writer = XlsxWriter()
|
|
69
|
+
|
|
70
|
+
def load_workbook(self, workbook, file_path: str, **options):
|
|
71
|
+
return self._reader.load_workbook(workbook, file_path, **options)
|
|
72
|
+
|
|
73
|
+
def save_workbook(self, workbook, file_path: str, **options):
|
|
74
|
+
return self._writer.save_workbook(workbook, file_path, **options)
|
|
75
|
+
|
|
76
|
+
FormatHandlerFactory.register('.xlsx', XlsxHandler)
|
|
77
|
+
FormatHandlerFactory.register('.xlsm', XlsxHandler)
|
|
78
|
+
FormatHandlerFactory.register('.xltx', XlsxHandler)
|
|
79
|
+
FormatHandlerFactory.register('.xltm', XlsxHandler)
|
|
80
|
+
|
|
81
|
+
# JSON Handler
|
|
82
|
+
class JsonHandler(IFormatHandler):
|
|
83
|
+
"""Handler for JSON format."""
|
|
84
|
+
|
|
85
|
+
def __init__(self):
|
|
86
|
+
from .json.reader import JsonReader
|
|
87
|
+
from .json.writer import JsonWriter
|
|
88
|
+
self._reader = JsonReader()
|
|
89
|
+
self._writer = JsonWriter()
|
|
90
|
+
|
|
91
|
+
def load_workbook(self, workbook, file_path: str, **options):
|
|
92
|
+
return self._reader.load_workbook(workbook, file_path, **options)
|
|
93
|
+
|
|
94
|
+
def save_workbook(self, workbook, file_path: str, **options):
|
|
95
|
+
return self._writer.save_workbook(workbook, file_path, **options)
|
|
96
|
+
|
|
97
|
+
FormatHandlerFactory.register('.json', JsonHandler)
|
|
98
|
+
|
|
99
|
+
# CSV Handler
|
|
100
|
+
class CsvHandler(IFormatHandler):
|
|
101
|
+
"""Handler for CSV format."""
|
|
102
|
+
|
|
103
|
+
def __init__(self):
|
|
104
|
+
from .csv.reader import CsvReader
|
|
105
|
+
from .csv.writer import CsvWriter
|
|
106
|
+
self._reader = CsvReader()
|
|
107
|
+
self._writer = CsvWriter()
|
|
108
|
+
|
|
109
|
+
def load_workbook(self, workbook, file_path: str, **options):
|
|
110
|
+
return self._reader.load_workbook(workbook, file_path, **options)
|
|
111
|
+
|
|
112
|
+
def save_workbook(self, workbook, file_path: str, **options):
|
|
113
|
+
return self._writer.save_workbook(workbook, file_path, **options)
|
|
114
|
+
|
|
115
|
+
FormatHandlerFactory.register('.csv', CsvHandler)
|
|
116
|
+
|
|
117
|
+
# Markdown Handler
|
|
118
|
+
class MarkdownHandler(IFormatHandler):
|
|
119
|
+
"""Handler for Markdown format."""
|
|
120
|
+
|
|
121
|
+
def __init__(self):
|
|
122
|
+
from .md.reader import MarkdownReader
|
|
123
|
+
from .md.writer import MarkdownWriter
|
|
124
|
+
self._reader = MarkdownReader()
|
|
125
|
+
self._writer = MarkdownWriter()
|
|
126
|
+
|
|
127
|
+
def load_workbook(self, workbook, file_path: str, **options):
|
|
128
|
+
return self._reader.load_workbook(workbook, file_path, **options)
|
|
129
|
+
|
|
130
|
+
def save_workbook(self, workbook, file_path: str, **options):
|
|
131
|
+
return self._writer.save_workbook(workbook, file_path, **options)
|
|
132
|
+
|
|
133
|
+
FormatHandlerFactory.register('.md', MarkdownHandler)
|
|
134
|
+
FormatHandlerFactory.register('.markdown', MarkdownHandler)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Initialize built-in formats
|
|
138
|
+
_register_builtin_formats()
|