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
aspose/cells/range.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Range implementation for operating on multiple cells as a group.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Iterator, Tuple, Union, TYPE_CHECKING
|
|
6
|
+
from .formats import CellValue
|
|
7
|
+
from .utils import parse_range, InvalidCoordinateError
|
|
8
|
+
from .style import Style, Font, Fill
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .worksheet import Worksheet
|
|
12
|
+
from .cell import Cell
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Range:
|
|
16
|
+
"""Excel range representing a rectangular area of cells."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, worksheet: 'Worksheet', range_string: str):
|
|
19
|
+
self._worksheet = worksheet
|
|
20
|
+
self._range_string = range_string.upper()
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
(self._start_row, self._start_col), (self._end_row, self._end_col) = parse_range(range_string)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
raise InvalidCoordinateError(f"Invalid range format: {range_string}") from e
|
|
26
|
+
|
|
27
|
+
# Ensure start <= end
|
|
28
|
+
if self._start_row > self._end_row:
|
|
29
|
+
self._start_row, self._end_row = self._end_row, self._start_row
|
|
30
|
+
if self._start_col > self._end_col:
|
|
31
|
+
self._start_col, self._end_col = self._end_col, self._start_col
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def coordinate(self) -> str:
|
|
35
|
+
"""Range coordinate string."""
|
|
36
|
+
return self._range_string
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def min_row(self) -> int:
|
|
40
|
+
"""Minimum row in range."""
|
|
41
|
+
return self._start_row
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def max_row(self) -> int:
|
|
45
|
+
"""Maximum row in range."""
|
|
46
|
+
return self._end_row
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def min_column(self) -> int:
|
|
50
|
+
"""Minimum column in range."""
|
|
51
|
+
return self._start_col
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def max_column(self) -> int:
|
|
55
|
+
"""Maximum column in range."""
|
|
56
|
+
return self._end_col
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def row_count(self) -> int:
|
|
60
|
+
"""Number of rows in range."""
|
|
61
|
+
return self._end_row - self._start_row + 1
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def column_count(self) -> int:
|
|
65
|
+
"""Number of columns in range."""
|
|
66
|
+
return self._end_col - self._start_col + 1
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def size(self) -> Tuple[int, int]:
|
|
70
|
+
"""Range size as (rows, columns)."""
|
|
71
|
+
return self.row_count, self.column_count
|
|
72
|
+
|
|
73
|
+
def cells(self) -> Iterator['Cell']:
|
|
74
|
+
"""Iterate over all cells in range."""
|
|
75
|
+
for row in range(self._start_row, self._end_row + 1):
|
|
76
|
+
for col in range(self._start_col, self._end_col + 1):
|
|
77
|
+
yield self._worksheet.cell(row, col)
|
|
78
|
+
|
|
79
|
+
def rows_iter(self) -> Iterator[List['Cell']]:
|
|
80
|
+
"""Iterate over rows of cells."""
|
|
81
|
+
for row in range(self._start_row, self._end_row + 1):
|
|
82
|
+
row_cells = []
|
|
83
|
+
for col in range(self._start_col, self._end_col + 1):
|
|
84
|
+
row_cells.append(self._worksheet.cell(row, col))
|
|
85
|
+
yield row_cells
|
|
86
|
+
|
|
87
|
+
def columns_iter(self) -> Iterator[List['Cell']]:
|
|
88
|
+
"""Iterate over columns of cells."""
|
|
89
|
+
for col in range(self._start_col, self._end_col + 1):
|
|
90
|
+
col_cells = []
|
|
91
|
+
for row in range(self._start_row, self._end_row + 1):
|
|
92
|
+
col_cells.append(self._worksheet.cell(row, col))
|
|
93
|
+
yield col_cells
|
|
94
|
+
|
|
95
|
+
def rows(self) -> Iterator[List['Cell']]:
|
|
96
|
+
"""Iterate over rows of cells (alias for rows_iter for test compatibility)."""
|
|
97
|
+
return self.rows_iter()
|
|
98
|
+
|
|
99
|
+
def columns(self) -> Iterator[List['Cell']]:
|
|
100
|
+
"""Iterate over columns of cells (alias for columns_iter for test compatibility)."""
|
|
101
|
+
return self.columns_iter()
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def values(self) -> List[List[CellValue]]:
|
|
105
|
+
"""Get all values as nested list."""
|
|
106
|
+
result = []
|
|
107
|
+
for row in range(self._start_row, self._end_row + 1):
|
|
108
|
+
row_values = []
|
|
109
|
+
for col in range(self._start_col, self._end_col + 1):
|
|
110
|
+
cell = self._worksheet.cell(row, col)
|
|
111
|
+
row_values.append(cell.value)
|
|
112
|
+
result.append(row_values)
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
@values.setter
|
|
116
|
+
def values(self, data: Union[List[List[CellValue]], List[CellValue], CellValue]):
|
|
117
|
+
"""Set values from nested list or single value."""
|
|
118
|
+
if not isinstance(data, list):
|
|
119
|
+
# Single value - apply to all cells
|
|
120
|
+
for cell in self.cells():
|
|
121
|
+
cell.value = data
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
if not data:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# Check if it's a list of lists (2D) or single list (1D)
|
|
128
|
+
if isinstance(data[0], list):
|
|
129
|
+
# 2D data
|
|
130
|
+
for row_idx, row_data in enumerate(data):
|
|
131
|
+
if row_idx >= self.row_count:
|
|
132
|
+
break
|
|
133
|
+
for col_idx, value in enumerate(row_data):
|
|
134
|
+
if col_idx >= self.column_count:
|
|
135
|
+
break
|
|
136
|
+
cell = self._worksheet.cell(
|
|
137
|
+
self._start_row + row_idx,
|
|
138
|
+
self._start_col + col_idx
|
|
139
|
+
)
|
|
140
|
+
cell.value = value
|
|
141
|
+
else:
|
|
142
|
+
# 1D data - fill row by row
|
|
143
|
+
flat_index = 0
|
|
144
|
+
for row in range(self._start_row, self._end_row + 1):
|
|
145
|
+
for col in range(self._start_col, self._end_col + 1):
|
|
146
|
+
if flat_index >= len(data):
|
|
147
|
+
return
|
|
148
|
+
cell = self._worksheet.cell(row, col)
|
|
149
|
+
cell.value = data[flat_index]
|
|
150
|
+
flat_index += 1
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def font(self) -> Font:
|
|
154
|
+
"""Get font of first cell (for styling entire range)."""
|
|
155
|
+
first_cell = self._worksheet.cell(self._start_row, self._start_col)
|
|
156
|
+
return first_cell.font
|
|
157
|
+
|
|
158
|
+
@font.setter
|
|
159
|
+
def font(self, value: Font):
|
|
160
|
+
"""Apply font to entire range."""
|
|
161
|
+
for cell in self.cells():
|
|
162
|
+
cell.style.font = value.copy()
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def fill(self) -> Fill:
|
|
166
|
+
"""Get fill of first cell (for styling entire range)."""
|
|
167
|
+
first_cell = self._worksheet.cell(self._start_row, self._start_col)
|
|
168
|
+
return first_cell.fill
|
|
169
|
+
|
|
170
|
+
@fill.setter
|
|
171
|
+
def fill(self, value: Fill):
|
|
172
|
+
"""Apply fill to entire range."""
|
|
173
|
+
for cell in self.cells():
|
|
174
|
+
cell.style.fill = value.copy()
|
|
175
|
+
|
|
176
|
+
def apply_style(self, style: Style):
|
|
177
|
+
"""Apply complete style to entire range."""
|
|
178
|
+
for cell in self.cells():
|
|
179
|
+
cell.style = style.copy()
|
|
180
|
+
|
|
181
|
+
def clear(self):
|
|
182
|
+
"""Clear all values and formatting in range."""
|
|
183
|
+
for cell in self.cells():
|
|
184
|
+
cell.clear()
|
|
185
|
+
|
|
186
|
+
def merge(self):
|
|
187
|
+
"""Mark range for merging (implementation depends on writer)."""
|
|
188
|
+
if hasattr(self._worksheet, '_merged_ranges'):
|
|
189
|
+
self._worksheet._merged_ranges.add(self._range_string)
|
|
190
|
+
|
|
191
|
+
def unmerge(self):
|
|
192
|
+
"""Unmerge previously merged range."""
|
|
193
|
+
if hasattr(self._worksheet, '_merged_ranges'):
|
|
194
|
+
self._worksheet._merged_ranges.discard(self._range_string)
|
|
195
|
+
|
|
196
|
+
def __str__(self) -> str:
|
|
197
|
+
"""String representation."""
|
|
198
|
+
return f"Range({self._range_string})"
|
|
199
|
+
|
|
200
|
+
def __repr__(self) -> str:
|
|
201
|
+
"""Debug representation."""
|
|
202
|
+
return f"Range('{self._range_string}', {self.row_count}x{self.column_count})"
|
|
203
|
+
|
|
204
|
+
def __iter__(self) -> Iterator['Cell']:
|
|
205
|
+
"""Iterate over cells."""
|
|
206
|
+
return self.cells()
|
|
207
|
+
|
|
208
|
+
def __len__(self) -> int:
|
|
209
|
+
"""Number of cells in range."""
|
|
210
|
+
return self.row_count * self.column_count
|
aspose/cells/style.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simplified styling system for Excel cells and ranges.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Font:
|
|
9
|
+
"""Font styling properties."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.name: str = "Calibri"
|
|
13
|
+
self.size: Union[int, float] = 11
|
|
14
|
+
self.bold: bool = False
|
|
15
|
+
self.italic: bool = False
|
|
16
|
+
self.underline: bool = False
|
|
17
|
+
self.color: str = "black"
|
|
18
|
+
|
|
19
|
+
def copy(self) -> 'Font':
|
|
20
|
+
"""Create a copy of this font."""
|
|
21
|
+
new_font = Font()
|
|
22
|
+
new_font.name = self.name
|
|
23
|
+
new_font.size = self.size
|
|
24
|
+
new_font.bold = self.bold
|
|
25
|
+
new_font.italic = self.italic
|
|
26
|
+
new_font.underline = self.underline
|
|
27
|
+
new_font.color = self.color
|
|
28
|
+
return new_font
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Fill:
|
|
32
|
+
"""Fill/background styling properties."""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.color: str = "white"
|
|
36
|
+
self.pattern: str = "none"
|
|
37
|
+
self.gradient: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
def copy(self) -> 'Fill':
|
|
40
|
+
"""Create a copy of this fill."""
|
|
41
|
+
new_fill = Fill()
|
|
42
|
+
new_fill.color = self.color
|
|
43
|
+
new_fill.pattern = self.pattern
|
|
44
|
+
new_fill.gradient = self.gradient
|
|
45
|
+
return new_fill
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BorderSide:
|
|
49
|
+
"""Individual border side styling."""
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
self.style: str = "none" # none, thin, thick, medium, dashed, dotted, double
|
|
53
|
+
self.color: str = "black"
|
|
54
|
+
|
|
55
|
+
def copy(self) -> 'BorderSide':
|
|
56
|
+
"""Create a copy of this border side."""
|
|
57
|
+
new_side = BorderSide()
|
|
58
|
+
new_side.style = self.style
|
|
59
|
+
new_side.color = self.color
|
|
60
|
+
return new_side
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Border:
|
|
64
|
+
"""Comprehensive border styling properties."""
|
|
65
|
+
|
|
66
|
+
def __init__(self):
|
|
67
|
+
self._left: Optional[BorderSide] = None
|
|
68
|
+
self._right: Optional[BorderSide] = None
|
|
69
|
+
self._top: Optional[BorderSide] = None
|
|
70
|
+
self._bottom: Optional[BorderSide] = None
|
|
71
|
+
self._diagonal: Optional[BorderSide] = None
|
|
72
|
+
self._diagonal_up: bool = False
|
|
73
|
+
self._diagonal_down: bool = False
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def left(self) -> BorderSide:
|
|
77
|
+
"""Get or create left border."""
|
|
78
|
+
if self._left is None:
|
|
79
|
+
self._left = BorderSide()
|
|
80
|
+
return self._left
|
|
81
|
+
|
|
82
|
+
@left.setter
|
|
83
|
+
def left(self, value: BorderSide):
|
|
84
|
+
"""Set left border."""
|
|
85
|
+
self._left = value
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def right(self) -> BorderSide:
|
|
89
|
+
"""Get or create right border."""
|
|
90
|
+
if self._right is None:
|
|
91
|
+
self._right = BorderSide()
|
|
92
|
+
return self._right
|
|
93
|
+
|
|
94
|
+
@right.setter
|
|
95
|
+
def right(self, value: BorderSide):
|
|
96
|
+
"""Set right border."""
|
|
97
|
+
self._right = value
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def top(self) -> BorderSide:
|
|
101
|
+
"""Get or create top border."""
|
|
102
|
+
if self._top is None:
|
|
103
|
+
self._top = BorderSide()
|
|
104
|
+
return self._top
|
|
105
|
+
|
|
106
|
+
@top.setter
|
|
107
|
+
def top(self, value: BorderSide):
|
|
108
|
+
"""Set top border."""
|
|
109
|
+
self._top = value
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def bottom(self) -> BorderSide:
|
|
113
|
+
"""Get or create bottom border."""
|
|
114
|
+
if self._bottom is None:
|
|
115
|
+
self._bottom = BorderSide()
|
|
116
|
+
return self._bottom
|
|
117
|
+
|
|
118
|
+
@bottom.setter
|
|
119
|
+
def bottom(self, value: BorderSide):
|
|
120
|
+
"""Set bottom border."""
|
|
121
|
+
self._bottom = value
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def diagonal(self) -> BorderSide:
|
|
125
|
+
"""Get or create diagonal border."""
|
|
126
|
+
if self._diagonal is None:
|
|
127
|
+
self._diagonal = BorderSide()
|
|
128
|
+
return self._diagonal
|
|
129
|
+
|
|
130
|
+
@diagonal.setter
|
|
131
|
+
def diagonal(self, value: BorderSide):
|
|
132
|
+
"""Set diagonal border."""
|
|
133
|
+
self._diagonal = value
|
|
134
|
+
|
|
135
|
+
def set_all_borders(self, style: str = "thin", color: str = "black"):
|
|
136
|
+
"""Set all borders to the same style and color."""
|
|
137
|
+
for side in ['left', 'right', 'top', 'bottom']:
|
|
138
|
+
border_side = getattr(self, side)
|
|
139
|
+
border_side.style = style
|
|
140
|
+
border_side.color = color
|
|
141
|
+
|
|
142
|
+
def set_outline(self, style: str = "thin", color: str = "black"):
|
|
143
|
+
"""Set outline borders (all four sides)."""
|
|
144
|
+
self.set_all_borders(style, color)
|
|
145
|
+
|
|
146
|
+
def remove_all_borders(self):
|
|
147
|
+
"""Remove all borders."""
|
|
148
|
+
self._left = None
|
|
149
|
+
self._right = None
|
|
150
|
+
self._top = None
|
|
151
|
+
self._bottom = None
|
|
152
|
+
self._diagonal = None
|
|
153
|
+
|
|
154
|
+
def copy(self) -> 'Border':
|
|
155
|
+
"""Create a copy of this border."""
|
|
156
|
+
new_border = Border()
|
|
157
|
+
if self._left:
|
|
158
|
+
new_border._left = self._left.copy()
|
|
159
|
+
if self._right:
|
|
160
|
+
new_border._right = self._right.copy()
|
|
161
|
+
if self._top:
|
|
162
|
+
new_border._top = self._top.copy()
|
|
163
|
+
if self._bottom:
|
|
164
|
+
new_border._bottom = self._bottom.copy()
|
|
165
|
+
if self._diagonal:
|
|
166
|
+
new_border._diagonal = self._diagonal.copy()
|
|
167
|
+
new_border._diagonal_up = self._diagonal_up
|
|
168
|
+
new_border._diagonal_down = self._diagonal_down
|
|
169
|
+
return new_border
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class Alignment:
|
|
173
|
+
"""Text alignment properties."""
|
|
174
|
+
|
|
175
|
+
def __init__(self):
|
|
176
|
+
self.horizontal: str = "general"
|
|
177
|
+
self.vertical: str = "bottom"
|
|
178
|
+
self.wrap_text: bool = False
|
|
179
|
+
self.shrink_to_fit: bool = False
|
|
180
|
+
self.indent: int = 0
|
|
181
|
+
self.text_rotation: int = 0
|
|
182
|
+
|
|
183
|
+
def copy(self) -> 'Alignment':
|
|
184
|
+
"""Create a copy of this alignment."""
|
|
185
|
+
new_alignment = Alignment()
|
|
186
|
+
new_alignment.horizontal = self.horizontal
|
|
187
|
+
new_alignment.vertical = self.vertical
|
|
188
|
+
new_alignment.wrap_text = self.wrap_text
|
|
189
|
+
new_alignment.shrink_to_fit = self.shrink_to_fit
|
|
190
|
+
new_alignment.indent = self.indent
|
|
191
|
+
new_alignment.text_rotation = self.text_rotation
|
|
192
|
+
return new_alignment
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class Style:
|
|
196
|
+
"""Complete cell style container."""
|
|
197
|
+
|
|
198
|
+
def __init__(self):
|
|
199
|
+
self._font: Optional[Font] = None
|
|
200
|
+
self._fill: Optional[Fill] = None
|
|
201
|
+
self._border: Optional[Border] = None
|
|
202
|
+
self._alignment: Optional[Alignment] = None
|
|
203
|
+
self._number_format: str = "General"
|
|
204
|
+
self._protection: bool = True
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def font(self) -> Font:
|
|
208
|
+
"""Get or create font styling."""
|
|
209
|
+
if self._font is None:
|
|
210
|
+
self._font = Font()
|
|
211
|
+
return self._font
|
|
212
|
+
|
|
213
|
+
@font.setter
|
|
214
|
+
def font(self, value: Font):
|
|
215
|
+
"""Set font styling."""
|
|
216
|
+
self._font = value
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def fill(self) -> Fill:
|
|
220
|
+
"""Get or create fill styling."""
|
|
221
|
+
if self._fill is None:
|
|
222
|
+
self._fill = Fill()
|
|
223
|
+
return self._fill
|
|
224
|
+
|
|
225
|
+
@fill.setter
|
|
226
|
+
def fill(self, value: Fill):
|
|
227
|
+
"""Set fill styling."""
|
|
228
|
+
self._fill = value
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def border(self) -> Border:
|
|
232
|
+
"""Get or create border styling."""
|
|
233
|
+
if self._border is None:
|
|
234
|
+
self._border = Border()
|
|
235
|
+
return self._border
|
|
236
|
+
|
|
237
|
+
@border.setter
|
|
238
|
+
def border(self, value: Border):
|
|
239
|
+
"""Set border styling."""
|
|
240
|
+
self._border = value
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def alignment(self) -> Alignment:
|
|
244
|
+
"""Get or create alignment styling."""
|
|
245
|
+
if self._alignment is None:
|
|
246
|
+
self._alignment = Alignment()
|
|
247
|
+
return self._alignment
|
|
248
|
+
|
|
249
|
+
@alignment.setter
|
|
250
|
+
def alignment(self, value: Alignment):
|
|
251
|
+
"""Set alignment styling."""
|
|
252
|
+
self._alignment = value
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def number_format(self) -> str:
|
|
256
|
+
"""Get number format."""
|
|
257
|
+
return self._number_format
|
|
258
|
+
|
|
259
|
+
@number_format.setter
|
|
260
|
+
def number_format(self, value: str):
|
|
261
|
+
"""Set number format."""
|
|
262
|
+
self._number_format = value
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def protection(self) -> bool:
|
|
266
|
+
"""Get protection status."""
|
|
267
|
+
return self._protection
|
|
268
|
+
|
|
269
|
+
@protection.setter
|
|
270
|
+
def protection(self, value: bool):
|
|
271
|
+
"""Set protection status."""
|
|
272
|
+
self._protection = value
|
|
273
|
+
|
|
274
|
+
def copy(self) -> 'Style':
|
|
275
|
+
"""Create a deep copy of this style."""
|
|
276
|
+
new_style = Style()
|
|
277
|
+
if self._font:
|
|
278
|
+
new_style._font = self._font.copy()
|
|
279
|
+
if self._fill:
|
|
280
|
+
new_style._fill = self._fill.copy()
|
|
281
|
+
if self._border:
|
|
282
|
+
new_style._border = self._border.copy()
|
|
283
|
+
if self._alignment:
|
|
284
|
+
new_style._alignment = self._alignment.copy()
|
|
285
|
+
new_style._number_format = self._number_format
|
|
286
|
+
new_style._protection = self._protection
|
|
287
|
+
return new_style
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions and classes for Excel operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .coordinates import (
|
|
6
|
+
column_index_to_letter,
|
|
7
|
+
column_letter_to_index,
|
|
8
|
+
coordinate_to_tuple,
|
|
9
|
+
tuple_to_coordinate,
|
|
10
|
+
parse_range
|
|
11
|
+
)
|
|
12
|
+
from .validation import (
|
|
13
|
+
is_numeric_string,
|
|
14
|
+
is_formula,
|
|
15
|
+
is_date_value,
|
|
16
|
+
infer_data_type,
|
|
17
|
+
validate_sheet_name,
|
|
18
|
+
sanitize_sheet_name,
|
|
19
|
+
convert_value
|
|
20
|
+
)
|
|
21
|
+
from .exceptions import (
|
|
22
|
+
AsposeException,
|
|
23
|
+
FileFormatError,
|
|
24
|
+
InvalidCoordinateError,
|
|
25
|
+
WorksheetNotFoundError,
|
|
26
|
+
CellValueError,
|
|
27
|
+
ExportError
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
# Coordinates
|
|
32
|
+
"column_index_to_letter",
|
|
33
|
+
"column_letter_to_index",
|
|
34
|
+
"coordinate_to_tuple",
|
|
35
|
+
"tuple_to_coordinate",
|
|
36
|
+
"parse_range",
|
|
37
|
+
|
|
38
|
+
# Validation
|
|
39
|
+
"is_numeric_string",
|
|
40
|
+
"is_formula",
|
|
41
|
+
"is_date_value",
|
|
42
|
+
"infer_data_type",
|
|
43
|
+
"validate_sheet_name",
|
|
44
|
+
"sanitize_sheet_name",
|
|
45
|
+
"convert_value",
|
|
46
|
+
|
|
47
|
+
# Exceptions
|
|
48
|
+
"AsposeException",
|
|
49
|
+
"FileFormatError",
|
|
50
|
+
"InvalidCoordinateError",
|
|
51
|
+
"WorksheetNotFoundError",
|
|
52
|
+
"CellValueError",
|
|
53
|
+
"ExportError"
|
|
54
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Coordinate conversion utilities for Excel cell addressing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
from .exceptions import InvalidCoordinateError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def column_index_to_letter(index: int) -> str:
|
|
11
|
+
"""Convert 1-based column index to Excel letter (1 -> A, 27 -> AA)."""
|
|
12
|
+
if index < 1:
|
|
13
|
+
raise InvalidCoordinateError(f"Column index must be >= 1, got {index}")
|
|
14
|
+
|
|
15
|
+
result = ""
|
|
16
|
+
while index > 0:
|
|
17
|
+
index -= 1
|
|
18
|
+
result = chr(65 + index % 26) + result
|
|
19
|
+
index //= 26
|
|
20
|
+
return result
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def column_letter_to_index(letter: str) -> int:
|
|
24
|
+
"""Convert Excel column letter to 1-based index (A -> 1, AA -> 27)."""
|
|
25
|
+
if not letter or not letter.isalpha():
|
|
26
|
+
raise InvalidCoordinateError(f"Invalid column letter: {letter}")
|
|
27
|
+
|
|
28
|
+
result = 0
|
|
29
|
+
for char in letter.upper():
|
|
30
|
+
result = result * 26 + (ord(char) - ord('A') + 1)
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def coordinate_to_tuple(coordinate: str) -> Tuple[int, int]:
|
|
35
|
+
"""Convert Excel coordinate to (row, column) tuple (A1 -> (1, 1))."""
|
|
36
|
+
match = re.match(r'^([A-Z]+)(\d+)$', coordinate.upper())
|
|
37
|
+
if not match:
|
|
38
|
+
raise InvalidCoordinateError(f"Invalid coordinate format: {coordinate}")
|
|
39
|
+
|
|
40
|
+
col_letter, row_str = match.groups()
|
|
41
|
+
row = int(row_str)
|
|
42
|
+
col = column_letter_to_index(col_letter)
|
|
43
|
+
|
|
44
|
+
if row < 1:
|
|
45
|
+
raise InvalidCoordinateError(f"Row number must be >= 1, got {row}")
|
|
46
|
+
|
|
47
|
+
return row, col
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def tuple_to_coordinate(row: int, column: int) -> str:
|
|
51
|
+
"""Convert (row, column) tuple to Excel coordinate ((1, 1) -> A1)."""
|
|
52
|
+
if row < 1 or column < 1:
|
|
53
|
+
raise InvalidCoordinateError(f"Row and column must be >= 1, got ({row}, {column})")
|
|
54
|
+
|
|
55
|
+
col_letter = column_index_to_letter(column)
|
|
56
|
+
return f"{col_letter}{row}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_range(range_str: str) -> Tuple[Tuple[int, int], Tuple[int, int]]:
|
|
60
|
+
"""Parse Excel range to ((start_row, start_col), (end_row, end_col))."""
|
|
61
|
+
if ':' not in range_str:
|
|
62
|
+
raise InvalidCoordinateError(f"Range must contain ':', got: {range_str}")
|
|
63
|
+
|
|
64
|
+
start_cell, end_cell = range_str.split(':', 1)
|
|
65
|
+
start_coord = coordinate_to_tuple(start_cell.strip())
|
|
66
|
+
end_coord = coordinate_to_tuple(end_cell.strip())
|
|
67
|
+
|
|
68
|
+
return start_coord, end_coord
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exception classes for Aspose.Cells operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AsposeException(Exception):
|
|
7
|
+
"""Base exception for all Aspose.Cells errors."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FileFormatError(AsposeException):
|
|
12
|
+
"""Raised when file format is not supported or invalid."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InvalidCoordinateError(AsposeException):
|
|
17
|
+
"""Raised when cell coordinate is invalid."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WorksheetNotFoundError(AsposeException):
|
|
22
|
+
"""Raised when worksheet cannot be found."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CellValueError(AsposeException):
|
|
27
|
+
"""Raised when cell value operation fails."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ExportError(AsposeException):
|
|
32
|
+
"""Raised when data export operation fails."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ExcelValidationError(AsposeException):
|
|
37
|
+
"""Raised when Excel data validation fails."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CellRangeError(AsposeException):
|
|
42
|
+
"""Raised when cell range operation fails."""
|
|
43
|
+
pass
|