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,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data validation utilities for Excel operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Union
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from ..formats import CellValue
|
|
10
|
+
from ..constants import (
|
|
11
|
+
MAX_SHEET_NAME_LENGTH, INVALID_SHEET_NAME_CHARS,
|
|
12
|
+
CELL_REF_PATTERN, DEFAULT_SHEET_NAME
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_numeric_string(value: str) -> bool:
|
|
17
|
+
"""Check if string represents a numeric value."""
|
|
18
|
+
try:
|
|
19
|
+
float(value)
|
|
20
|
+
return True
|
|
21
|
+
except (ValueError, TypeError):
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_formula(value: str) -> bool:
|
|
26
|
+
"""Check if string represents an Excel formula."""
|
|
27
|
+
return isinstance(value, str) and value.startswith('=')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_date_value(value: CellValue) -> bool:
|
|
31
|
+
"""Check if value represents a date."""
|
|
32
|
+
return isinstance(value, datetime)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def infer_data_type(value: CellValue) -> str:
|
|
36
|
+
"""Infer Excel data type from Python value."""
|
|
37
|
+
if value is None:
|
|
38
|
+
return 'empty'
|
|
39
|
+
elif isinstance(value, bool):
|
|
40
|
+
return 'boolean'
|
|
41
|
+
elif isinstance(value, (int, float)):
|
|
42
|
+
return 'number'
|
|
43
|
+
elif isinstance(value, datetime):
|
|
44
|
+
return 'date'
|
|
45
|
+
elif isinstance(value, str):
|
|
46
|
+
if is_formula(value):
|
|
47
|
+
return 'formula'
|
|
48
|
+
elif is_numeric_string(value):
|
|
49
|
+
return 'number'
|
|
50
|
+
else:
|
|
51
|
+
return 'string'
|
|
52
|
+
else:
|
|
53
|
+
return 'string'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_sheet_name(name: str) -> bool:
|
|
57
|
+
"""Validate Excel worksheet name."""
|
|
58
|
+
if not name or len(name) > MAX_SHEET_NAME_LENGTH:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
return not any(char in name for char in INVALID_SHEET_NAME_CHARS)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def sanitize_sheet_name(name: str) -> str:
|
|
65
|
+
"""Sanitize worksheet name for Excel compatibility."""
|
|
66
|
+
if not name:
|
|
67
|
+
return DEFAULT_SHEET_NAME + "1"
|
|
68
|
+
|
|
69
|
+
# Remove invalid characters
|
|
70
|
+
for char in INVALID_SHEET_NAME_CHARS:
|
|
71
|
+
name = name.replace(char, '_')
|
|
72
|
+
|
|
73
|
+
# Truncate to maximum length
|
|
74
|
+
if len(name) > MAX_SHEET_NAME_LENGTH:
|
|
75
|
+
name = name[:MAX_SHEET_NAME_LENGTH]
|
|
76
|
+
|
|
77
|
+
return name or DEFAULT_SHEET_NAME + "1"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def validate_cell_reference(ref: str) -> bool:
|
|
81
|
+
"""Validate Excel cell reference format (e.g., A1, Z99, AA100)."""
|
|
82
|
+
if not ref or not isinstance(ref, str):
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
return bool(re.match(CELL_REF_PATTERN, ref.upper()))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def convert_value(value: CellValue, target_type: str, default: CellValue = None) -> CellValue:
|
|
89
|
+
"""Convert value to target type with fallback to default."""
|
|
90
|
+
try:
|
|
91
|
+
if target_type == 'string':
|
|
92
|
+
return str(value) if value is not None else ""
|
|
93
|
+
elif target_type == 'int':
|
|
94
|
+
return int(float(str(value)))
|
|
95
|
+
elif target_type == 'float':
|
|
96
|
+
return float(value)
|
|
97
|
+
elif target_type == 'bool':
|
|
98
|
+
return bool(value)
|
|
99
|
+
else:
|
|
100
|
+
return value
|
|
101
|
+
except (ValueError, TypeError):
|
|
102
|
+
return default
|
aspose/cells/workbook.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workbook implementation with unified API and multiple file format support.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional, Union
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .worksheet import Worksheet
|
|
9
|
+
from .formats import FileFormat, ConversionOptions
|
|
10
|
+
from .utils import (
|
|
11
|
+
sanitize_sheet_name,
|
|
12
|
+
WorksheetNotFoundError,
|
|
13
|
+
FileFormatError,
|
|
14
|
+
ExportError
|
|
15
|
+
)
|
|
16
|
+
from .io.factory import FormatHandlerFactory
|
|
17
|
+
from .io.models import WorkbookData
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WorksheetCollection:
|
|
23
|
+
"""Collection manager for worksheets with multiple access patterns."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, workbook: 'Workbook'):
|
|
26
|
+
self._workbook = workbook
|
|
27
|
+
|
|
28
|
+
def add(self, name: str) -> Worksheet:
|
|
29
|
+
"""Add new worksheet with specified name."""
|
|
30
|
+
clean_name = sanitize_sheet_name(name)
|
|
31
|
+
if clean_name in self._workbook._worksheets:
|
|
32
|
+
# Generate unique name
|
|
33
|
+
counter = 1
|
|
34
|
+
base_name = clean_name
|
|
35
|
+
while clean_name in self._workbook._worksheets:
|
|
36
|
+
clean_name = f"{base_name}_{counter}"
|
|
37
|
+
counter += 1
|
|
38
|
+
|
|
39
|
+
worksheet = Worksheet(self._workbook, clean_name)
|
|
40
|
+
self._workbook._worksheets[clean_name] = worksheet
|
|
41
|
+
return worksheet
|
|
42
|
+
|
|
43
|
+
def remove(self, name: Union[str, int, Worksheet]):
|
|
44
|
+
"""Remove worksheet by name, index, or object."""
|
|
45
|
+
if isinstance(name, Worksheet):
|
|
46
|
+
name = name.name
|
|
47
|
+
elif isinstance(name, int):
|
|
48
|
+
sheets = list(self._workbook._worksheets.values())
|
|
49
|
+
if 0 <= name < len(sheets):
|
|
50
|
+
name = sheets[name].name
|
|
51
|
+
else:
|
|
52
|
+
raise WorksheetNotFoundError(f"Worksheet index {name} out of range")
|
|
53
|
+
|
|
54
|
+
if name not in self._workbook._worksheets:
|
|
55
|
+
raise WorksheetNotFoundError(f"Worksheet '{name}' not found")
|
|
56
|
+
|
|
57
|
+
# Don't allow removing the last worksheet
|
|
58
|
+
if len(self._workbook._worksheets) <= 1:
|
|
59
|
+
raise WorksheetNotFoundError("Cannot remove the last worksheet")
|
|
60
|
+
|
|
61
|
+
# Update active sheet if necessary
|
|
62
|
+
if self._workbook._active_sheet and self._workbook._active_sheet.name == name:
|
|
63
|
+
remaining_sheets = [ws for ws in self._workbook._worksheets.values() if ws.name != name]
|
|
64
|
+
self._workbook._active_sheet = remaining_sheets[0]
|
|
65
|
+
|
|
66
|
+
del self._workbook._worksheets[name]
|
|
67
|
+
|
|
68
|
+
def __getitem__(self, key: Union[str, int]) -> Worksheet:
|
|
69
|
+
"""Get worksheet by name or index."""
|
|
70
|
+
if isinstance(key, str):
|
|
71
|
+
if key not in self._workbook._worksheets:
|
|
72
|
+
raise WorksheetNotFoundError(f"Worksheet '{key}' not found")
|
|
73
|
+
return self._workbook._worksheets[key]
|
|
74
|
+
elif isinstance(key, int):
|
|
75
|
+
sheets = list(self._workbook._worksheets.values())
|
|
76
|
+
if 0 <= key < len(sheets):
|
|
77
|
+
return sheets[key]
|
|
78
|
+
else:
|
|
79
|
+
raise WorksheetNotFoundError(f"Worksheet index {key} out of range")
|
|
80
|
+
else:
|
|
81
|
+
raise WorksheetNotFoundError(f"Invalid worksheet key: {key}")
|
|
82
|
+
|
|
83
|
+
def __len__(self) -> int:
|
|
84
|
+
"""Number of worksheets."""
|
|
85
|
+
return len(self._workbook._worksheets)
|
|
86
|
+
|
|
87
|
+
def __iter__(self):
|
|
88
|
+
"""Iterate over worksheets."""
|
|
89
|
+
return iter(self._workbook._worksheets.values())
|
|
90
|
+
|
|
91
|
+
def __contains__(self, name: str) -> bool:
|
|
92
|
+
"""Check if worksheet exists."""
|
|
93
|
+
return name in self._workbook._worksheets
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Workbook:
|
|
97
|
+
"""Excel workbook with unified API and multiple access patterns."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, filename: Optional[Union[str, Path]] = None):
|
|
100
|
+
self._filename: Optional[Path] = None
|
|
101
|
+
self._worksheets: Dict[str, Worksheet] = {}
|
|
102
|
+
self._active_sheet: Optional[Worksheet] = None
|
|
103
|
+
self._shared_strings: List[str] = []
|
|
104
|
+
self._properties: Dict[str, Union[str, int, float, bool]] = {}
|
|
105
|
+
|
|
106
|
+
# Initialize with default worksheet
|
|
107
|
+
default_sheet = Worksheet(self, "Sheet1")
|
|
108
|
+
self._worksheets["Sheet1"] = default_sheet
|
|
109
|
+
self._active_sheet = default_sheet
|
|
110
|
+
|
|
111
|
+
if filename:
|
|
112
|
+
self._load_from_file(filename)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def load(cls, filename: Union[str, Path]) -> 'Workbook':
|
|
116
|
+
"""Load workbook from file."""
|
|
117
|
+
return cls(filename)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def active(self) -> Worksheet:
|
|
121
|
+
"""Get active worksheet."""
|
|
122
|
+
if self._active_sheet is None and self._worksheets:
|
|
123
|
+
self._active_sheet = next(iter(self._worksheets.values()))
|
|
124
|
+
return self._active_sheet
|
|
125
|
+
|
|
126
|
+
@active.setter
|
|
127
|
+
def active(self, value: Union[Worksheet, str, int]):
|
|
128
|
+
"""Set active worksheet by object, name, or index."""
|
|
129
|
+
if isinstance(value, Worksheet):
|
|
130
|
+
if value in self._worksheets.values():
|
|
131
|
+
self._active_sheet = value
|
|
132
|
+
else:
|
|
133
|
+
raise WorksheetNotFoundError("Worksheet not in this workbook")
|
|
134
|
+
elif isinstance(value, str):
|
|
135
|
+
if value in self._worksheets:
|
|
136
|
+
self._active_sheet = self._worksheets[value]
|
|
137
|
+
else:
|
|
138
|
+
raise WorksheetNotFoundError(f"Worksheet '{value}' not found")
|
|
139
|
+
elif isinstance(value, int):
|
|
140
|
+
sheets = list(self._worksheets.values())
|
|
141
|
+
if 0 <= value < len(sheets):
|
|
142
|
+
self._active_sheet = sheets[value]
|
|
143
|
+
else:
|
|
144
|
+
raise WorksheetNotFoundError(f"Worksheet index {value} out of range")
|
|
145
|
+
else:
|
|
146
|
+
raise WorksheetNotFoundError(f"Invalid active sheet value: {value}")
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def worksheets(self) -> WorksheetCollection:
|
|
150
|
+
"""Get worksheet collection manager."""
|
|
151
|
+
return WorksheetCollection(self)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def sheetnames(self) -> List[str]:
|
|
155
|
+
"""Get list of worksheet names."""
|
|
156
|
+
return list(self._worksheets.keys())
|
|
157
|
+
|
|
158
|
+
def create_sheet(self, name: str = None, index: int = None) -> Worksheet:
|
|
159
|
+
"""Create new worksheet with optional name and position."""
|
|
160
|
+
if name is None:
|
|
161
|
+
# Generate default name
|
|
162
|
+
counter = len(self._worksheets) + 1
|
|
163
|
+
while f"Sheet{counter}" in self._worksheets:
|
|
164
|
+
counter += 1
|
|
165
|
+
name = f"Sheet{counter}"
|
|
166
|
+
|
|
167
|
+
worksheet = self.worksheets.add(name)
|
|
168
|
+
|
|
169
|
+
# Handle index positioning if specified
|
|
170
|
+
if index is not None and 0 <= index < len(self._worksheets):
|
|
171
|
+
# Re-order worksheets to insert at specific position
|
|
172
|
+
sheet_items = list(self._worksheets.items())
|
|
173
|
+
# Remove the newly added sheet from its current position (last)
|
|
174
|
+
new_sheet_item = sheet_items.pop()
|
|
175
|
+
# Insert it at the specified index
|
|
176
|
+
sheet_items.insert(index, new_sheet_item)
|
|
177
|
+
# Rebuild the ordered dictionary
|
|
178
|
+
self._worksheets.clear()
|
|
179
|
+
for sheet_name, sheet_obj in sheet_items:
|
|
180
|
+
self._worksheets[sheet_name] = sheet_obj
|
|
181
|
+
|
|
182
|
+
return worksheet
|
|
183
|
+
|
|
184
|
+
def _load_from_file(self, filename: Union[str, Path]):
|
|
185
|
+
"""Load workbook from file using unified format factory."""
|
|
186
|
+
self._filename = Path(filename)
|
|
187
|
+
|
|
188
|
+
if not self._filename.exists():
|
|
189
|
+
raise FileFormatError(f"File not found: {filename}")
|
|
190
|
+
|
|
191
|
+
# Try unified format handler first
|
|
192
|
+
handler = FormatHandlerFactory.get_handler(str(filename))
|
|
193
|
+
if handler:
|
|
194
|
+
handler.load_workbook(self, str(filename))
|
|
195
|
+
else:
|
|
196
|
+
# Fall back to legacy reader for unsupported formats
|
|
197
|
+
if self._filename.suffix.lower() not in ['.xlsx', '.xlsm', '.xltx', '.xltm']:
|
|
198
|
+
raise FileFormatError(f"Unsupported file format: {self._filename.suffix}")
|
|
199
|
+
|
|
200
|
+
from .io.xlsx.reader import XlsxReader
|
|
201
|
+
reader = XlsxReader()
|
|
202
|
+
reader.load_workbook(self, str(filename))
|
|
203
|
+
|
|
204
|
+
def save(self, filename: Optional[Union[str, Path]] = None,
|
|
205
|
+
format: Optional[Union[str, FileFormat]] = None, **kwargs):
|
|
206
|
+
"""Save workbook to file with specified format using unified factory."""
|
|
207
|
+
if filename is None:
|
|
208
|
+
if self._filename is None:
|
|
209
|
+
raise FileFormatError("No filename specified and no previous filename available")
|
|
210
|
+
filename = self._filename
|
|
211
|
+
else:
|
|
212
|
+
filename = Path(filename)
|
|
213
|
+
|
|
214
|
+
# If format is specified, modify filename extension
|
|
215
|
+
if format is not None:
|
|
216
|
+
if isinstance(format, str):
|
|
217
|
+
# Convert string to FileFormat enum if possible
|
|
218
|
+
try:
|
|
219
|
+
format_enum = FileFormat(format)
|
|
220
|
+
filename = filename.with_suffix(format_enum.extension)
|
|
221
|
+
except ValueError:
|
|
222
|
+
# For unsupported format strings, try to use them directly
|
|
223
|
+
if not format.startswith('.'):
|
|
224
|
+
format = '.' + format
|
|
225
|
+
filename = filename.with_suffix(format)
|
|
226
|
+
else:
|
|
227
|
+
filename = filename.with_suffix(format.extension)
|
|
228
|
+
|
|
229
|
+
# Try unified format handler first
|
|
230
|
+
handler = FormatHandlerFactory.get_handler(str(filename))
|
|
231
|
+
if handler:
|
|
232
|
+
handler.save_workbook(self, str(filename), **kwargs)
|
|
233
|
+
else:
|
|
234
|
+
# Fall back to legacy writer
|
|
235
|
+
if format is None:
|
|
236
|
+
format = FileFormat.from_extension(filename)
|
|
237
|
+
elif isinstance(format, str):
|
|
238
|
+
try:
|
|
239
|
+
format = FileFormat(format)
|
|
240
|
+
except ValueError:
|
|
241
|
+
raise FileFormatError(f"Unsupported format: {format}")
|
|
242
|
+
|
|
243
|
+
from .io.xlsx.writer import XlsxWriter
|
|
244
|
+
writer = XlsxWriter()
|
|
245
|
+
writer.save_workbook(self, str(filename), **kwargs)
|
|
246
|
+
|
|
247
|
+
self._filename = Path(filename)
|
|
248
|
+
|
|
249
|
+
def exportAs(self, format: Union[str, FileFormat], **kwargs) -> str:
|
|
250
|
+
"""Export workbook as string in specified format."""
|
|
251
|
+
# Convert string to FileFormat enum if needed
|
|
252
|
+
if isinstance(format, str):
|
|
253
|
+
try:
|
|
254
|
+
format_enum = FileFormat(format)
|
|
255
|
+
except ValueError:
|
|
256
|
+
raise ExportError(f"Unsupported export format: {format}")
|
|
257
|
+
else:
|
|
258
|
+
format_enum = format
|
|
259
|
+
|
|
260
|
+
if format_enum == FileFormat.JSON:
|
|
261
|
+
from .converters.json_converter import JsonConverter
|
|
262
|
+
converter = JsonConverter()
|
|
263
|
+
return converter.convert_workbook(self, **kwargs)
|
|
264
|
+
elif format_enum == FileFormat.CSV:
|
|
265
|
+
from .converters.csv_converter import CsvConverter
|
|
266
|
+
converter = CsvConverter()
|
|
267
|
+
return converter.convert_workbook(self, **kwargs)
|
|
268
|
+
elif format_enum == FileFormat.MARKDOWN:
|
|
269
|
+
from .converters.markdown_converter import MarkdownConverter
|
|
270
|
+
converter = MarkdownConverter()
|
|
271
|
+
return converter.convert_workbook(self, **kwargs)
|
|
272
|
+
else:
|
|
273
|
+
raise ExportError(f"Unsupported export format: {format_enum.value}")
|
|
274
|
+
|
|
275
|
+
def copy_worksheet(self, from_worksheet: Union[Worksheet, str]) -> Worksheet:
|
|
276
|
+
"""Create a copy of existing worksheet."""
|
|
277
|
+
if isinstance(from_worksheet, str):
|
|
278
|
+
if from_worksheet not in self._worksheets:
|
|
279
|
+
raise WorksheetNotFoundError(f"Source worksheet '{from_worksheet}' not found")
|
|
280
|
+
source = self._worksheets[from_worksheet]
|
|
281
|
+
else:
|
|
282
|
+
source = from_worksheet
|
|
283
|
+
|
|
284
|
+
# Generate new name
|
|
285
|
+
base_name = f"Copy of {source.name}"
|
|
286
|
+
new_name = base_name
|
|
287
|
+
counter = 1
|
|
288
|
+
while new_name in self._worksheets:
|
|
289
|
+
new_name = f"{base_name} ({counter})"
|
|
290
|
+
counter += 1
|
|
291
|
+
|
|
292
|
+
# Create new worksheet
|
|
293
|
+
new_worksheet = self.create_sheet(new_name)
|
|
294
|
+
|
|
295
|
+
# Copy all cell data and formatting
|
|
296
|
+
for coord, cell in source._cells.items():
|
|
297
|
+
row, col = coord
|
|
298
|
+
new_cell = new_worksheet.cell(row, col, cell.value)
|
|
299
|
+
if cell._style:
|
|
300
|
+
new_cell._style = cell._style.copy()
|
|
301
|
+
new_cell._number_format = cell._number_format
|
|
302
|
+
new_cell._hyperlink = cell._hyperlink
|
|
303
|
+
new_cell._comment = cell._comment
|
|
304
|
+
|
|
305
|
+
# Copy other properties
|
|
306
|
+
new_worksheet._merged_ranges = source._merged_ranges.copy()
|
|
307
|
+
new_worksheet._row_heights = source._row_heights.copy()
|
|
308
|
+
new_worksheet._column_widths = source._column_widths.copy()
|
|
309
|
+
new_worksheet._freeze_panes = source._freeze_panes
|
|
310
|
+
|
|
311
|
+
return new_worksheet
|
|
312
|
+
|
|
313
|
+
def convert_to(self, target_format: str, output_path: str, **options) -> None:
|
|
314
|
+
"""Convert workbook to different format using unified data model."""
|
|
315
|
+
# Convert workbook to unified data model
|
|
316
|
+
data = WorkbookData.from_workbook(self)
|
|
317
|
+
|
|
318
|
+
# Get target format handler
|
|
319
|
+
handler = FormatHandlerFactory.get_handler(output_path)
|
|
320
|
+
if not handler:
|
|
321
|
+
raise FileFormatError(f"Unsupported target format: {Path(output_path).suffix}")
|
|
322
|
+
|
|
323
|
+
# Write using unified data model
|
|
324
|
+
handler.write_from_data(data, output_path, **options)
|
|
325
|
+
|
|
326
|
+
def close(self):
|
|
327
|
+
"""Close workbook and release resources."""
|
|
328
|
+
self._worksheets.clear()
|
|
329
|
+
self._active_sheet = None
|
|
330
|
+
self._shared_strings.clear()
|
|
331
|
+
self._properties.clear()
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def properties(self) -> Dict[str, Union[str, int, float, bool]]:
|
|
335
|
+
"""Get workbook properties."""
|
|
336
|
+
return self._properties
|
|
337
|
+
|
|
338
|
+
def __enter__(self):
|
|
339
|
+
"""Context manager entry."""
|
|
340
|
+
return self
|
|
341
|
+
|
|
342
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
343
|
+
"""Context manager exit."""
|
|
344
|
+
self.close()
|
|
345
|
+
|
|
346
|
+
def __str__(self) -> str:
|
|
347
|
+
"""String representation."""
|
|
348
|
+
return f"Workbook({len(self._worksheets)} sheets)"
|
|
349
|
+
|
|
350
|
+
def __repr__(self) -> str:
|
|
351
|
+
"""Debug representation."""
|
|
352
|
+
return f"Workbook(sheets={list(self._worksheets.keys())}, active='{self.active.name if self.active else None}')"
|