pyfastexcel 0.1.8__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyfastexcel/__init__.py +21 -0
- pyfastexcel/_typing.py +31 -0
- pyfastexcel/chart.py +346 -0
- pyfastexcel/driver.py +290 -0
- pyfastexcel/enums.py +119 -0
- pyfastexcel/logformatter.py +38 -0
- pyfastexcel/pivot.py +124 -0
- pyfastexcel/pyfastexcel.dll +0 -0
- pyfastexcel/serializers.py +100 -0
- pyfastexcel/style.py +202 -0
- pyfastexcel/utils.py +184 -0
- pyfastexcel/validators.py +196 -0
- pyfastexcel/workbook.py +573 -0
- pyfastexcel/worksheet.py +1025 -0
- pyfastexcel/writer.py +148 -0
- pyfastexcel-0.1.8.dist-info/LICENSE +21 -0
- pyfastexcel-0.1.8.dist-info/METADATA +212 -0
- pyfastexcel-0.1.8.dist-info/RECORD +20 -0
- pyfastexcel-0.1.8.dist-info/WHEEL +5 -0
- pyfastexcel-0.1.8.dist-info/top_level.txt +1 -0
pyfastexcel/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from openpyxl_style_writer import CustomStyle, DefaultStyle
|
|
2
|
+
|
|
3
|
+
from pyfastexcel.workbook import Workbook
|
|
4
|
+
from pyfastexcel.writer import StreamWriter
|
|
5
|
+
from pyfastexcel.utils import set_debug_level
|
|
6
|
+
from pyfastexcel.enums import ChartType, ChartDataLabelPosition, ChartLineType, MarkerSymbol
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'Workbook',
|
|
10
|
+
'StreamWriter',
|
|
11
|
+
# Temporary link the CustomStyle from openpyxl_style_writer for
|
|
12
|
+
# convinent usage.
|
|
13
|
+
'CustomStyle',
|
|
14
|
+
'DefaultStyle',
|
|
15
|
+
'set_debug_level',
|
|
16
|
+
# Constants for chart creation.
|
|
17
|
+
'ChartType',
|
|
18
|
+
'ChartDataLabelPosition',
|
|
19
|
+
'ChartLineType',
|
|
20
|
+
'MarkerSymbol',
|
|
21
|
+
]
|
pyfastexcel/_typing.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Protocol, Optional, List, Union, TypeVar
|
|
4
|
+
from typing_extensions import TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CommentTextDict(TypedDict, total=False):
|
|
8
|
+
text: str
|
|
9
|
+
size: Optional[int]
|
|
10
|
+
name: Optional[str]
|
|
11
|
+
bold: Optional[bool]
|
|
12
|
+
italic: Optional[bool]
|
|
13
|
+
underline: Optional[Literal['single', 'double']]
|
|
14
|
+
strike: Optional[bool]
|
|
15
|
+
vert_align: Optional[str]
|
|
16
|
+
color: Optional[str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SelectionDict(TypedDict, total=False):
|
|
20
|
+
sq_ref: str
|
|
21
|
+
active_cell: str
|
|
22
|
+
pane: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Writable(Protocol):
|
|
26
|
+
def write(self, content: str) -> None: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Self = TypeVar('Self')
|
|
30
|
+
CommentTextStructure = Union[str, List[str], CommentTextDict, List[CommentTextDict]]
|
|
31
|
+
SetPanesSelection = List[SelectionDict]
|
pyfastexcel/chart.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, field_serializer
|
|
4
|
+
from typing import List, Literal, Optional
|
|
5
|
+
|
|
6
|
+
from .enums import ChartDataLabelPosition, ChartLineType, ChartType, MarkerSymbol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Font(BaseModel):
|
|
10
|
+
"""
|
|
11
|
+
Defines font settings for text elements in a chart.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
bold (Optional[bool]): Specifies if the text is bold.
|
|
15
|
+
color (Optional[str]): The color of the text.
|
|
16
|
+
family (Optional[str]): The font family for the text.
|
|
17
|
+
italic (Optional[bool]): Specifies if the text is italic.
|
|
18
|
+
size (Optional[float]): The font size for the text.
|
|
19
|
+
strike (Optional[bool]): Specifies if the text has a strikethrough.
|
|
20
|
+
underline (Optional[str]): The style of underline for the text.
|
|
21
|
+
vert_align (Optional[str]): Vertical alignment for the text, such as
|
|
22
|
+
"baseline", "superscript" or "subscript".
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
bold: Optional[bool] = Field(None, serialization_alias='Bold')
|
|
26
|
+
color: Optional[str] = Field(None, serialization_alias='Color')
|
|
27
|
+
family: Optional[str] = Field(None, serialization_alias='Family')
|
|
28
|
+
italic: Optional[bool] = Field(None, serialization_alias='Italic')
|
|
29
|
+
size: Optional[float] = Field(None, serialization_alias='Size')
|
|
30
|
+
strike: Optional[bool] = Field(None, serialization_alias='Strike')
|
|
31
|
+
underline: Optional[str] = Field(None, serialization_alias='Underline')
|
|
32
|
+
vert_align: Optional[str] = Field(None, serialization_alias='VertAlign')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Fill(BaseModel):
|
|
36
|
+
"""
|
|
37
|
+
Describes the fill settings.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
ftype (Optional[Literal['pattern', 'gradient']]): The type of fill, either
|
|
41
|
+
'pattern' or 'gradient'.
|
|
42
|
+
pattern (Optional[int]): The pattern index for fill (between 0 and 18).
|
|
43
|
+
color (Optional[str]): The fill color (Only support hex color value).
|
|
44
|
+
shading (Optional[int]): The shading index for the fill (between 0 and 5).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
ftype: Optional[Literal['pattern', 'gradient']] = Field('pattern', serialization_alias='Type')
|
|
48
|
+
pattern: Optional[int] = Field(None, serialization_alias='Pattern', gt=-1, lt=19)
|
|
49
|
+
color: Optional[str] = Field(None, serialization_alias='Color')
|
|
50
|
+
shading: Optional[int] = Field(None, serialization_alias='Shading', gt=-1, lt=6)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Marker(BaseModel):
|
|
54
|
+
"""
|
|
55
|
+
Defines the appearance and style of markers used in charts.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
fill (Optional[Fill]): Fill settings for the marker.
|
|
59
|
+
symbol (Optional[str | MarkerSymbol]): The symbol used for the marker.
|
|
60
|
+
size (Optional[int]): The size of the marker.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
fill: Optional[Fill] = Field(None, serialization_alias='Fill')
|
|
64
|
+
symbol: Optional[str | MarkerSymbol] = Field(None, serialization_alias='Symbol')
|
|
65
|
+
size: Optional[int] = Field(None, serialization_alias='Size')
|
|
66
|
+
|
|
67
|
+
@field_serializer('symbol')
|
|
68
|
+
@classmethod
|
|
69
|
+
def marker_symbol_serializer(cls, symbol: str | MarkerSymbol | None) -> str | None:
|
|
70
|
+
if symbol is None:
|
|
71
|
+
return None
|
|
72
|
+
if isinstance(symbol, MarkerSymbol):
|
|
73
|
+
return symbol.value
|
|
74
|
+
return MarkerSymbol.get_enum(symbol).value
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Line(BaseModel):
|
|
78
|
+
"""
|
|
79
|
+
Represents line settings for chart elements.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
ltype (Optional[str | ChartLineType]): The type of line.
|
|
83
|
+
smooth (Optional[bool]): Specifies if the line should be smoothed.
|
|
84
|
+
width (Optional[float]): The width of the line.
|
|
85
|
+
show_marker_line (Optional[bool]): Indicates if the line should be shown on markers.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
ltype: Optional[str | ChartLineType] = Field(None, serialization_alias='Type')
|
|
89
|
+
smooth: Optional[bool] = Field(None, serialization_alias='Smooth')
|
|
90
|
+
width: Optional[float] = Field(None, serialization_alias='Width')
|
|
91
|
+
show_marker_line: Optional[bool] = Field(None, serialization_alias='ShowMarkerLine')
|
|
92
|
+
|
|
93
|
+
@field_serializer('ltype')
|
|
94
|
+
@classmethod
|
|
95
|
+
def line_type_validator(cls, ltype: str | ChartLineType | None) -> int:
|
|
96
|
+
if ltype is None:
|
|
97
|
+
return None
|
|
98
|
+
if isinstance(ltype, ChartLineType):
|
|
99
|
+
return ltype.value
|
|
100
|
+
return ChartLineType.get_enum(ltype).value
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ChartLegend(BaseModel):
|
|
104
|
+
"""
|
|
105
|
+
Defines settings for the chart legend.
|
|
106
|
+
|
|
107
|
+
Attributes:
|
|
108
|
+
position (Optional[Literal['none', 'top', 'bottom', 'left', 'right', 'top_right']]):
|
|
109
|
+
The position of the legend.
|
|
110
|
+
show_legend_key (Optional[bool]): Specifies if the legend key should be shown.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
position: Optional[Literal['none', 'top', 'bottom', 'left', 'right', 'top_right']] = Field(
|
|
114
|
+
None, serialization_alias='Position'
|
|
115
|
+
)
|
|
116
|
+
show_legend_key: Optional[bool] = Field(None, serialization_alias='ShowLegendKey')
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class RichTextRun(BaseModel):
|
|
120
|
+
"""
|
|
121
|
+
Represents a text run with rich text formatting.
|
|
122
|
+
|
|
123
|
+
Attributes:
|
|
124
|
+
text (str): The text content.
|
|
125
|
+
font (Optional[Font]): Font settings for the text.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
text: str = Field(..., serialization_alias='Text')
|
|
129
|
+
font: Optional[Font] = Field(None, serialization_alias='Font')
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ChartCustomNumFmt(BaseModel):
|
|
133
|
+
"""
|
|
134
|
+
Custom number formatting for chart elements.
|
|
135
|
+
|
|
136
|
+
Attributes:
|
|
137
|
+
num_fmt (Optional[str]): The custom number format.
|
|
138
|
+
source_linked (Optional[bool]): Specifies if the format is linked to the source.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
num_fmt: Optional[str] = Field(None, serialization_alias='CustomNumFmt')
|
|
142
|
+
source_linked: Optional[bool] = Field(None, serialization_alias='SourceLinked')
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ChartAxis(BaseModel):
|
|
146
|
+
"""
|
|
147
|
+
Defines the settings for chart axes.
|
|
148
|
+
|
|
149
|
+
Attributes:
|
|
150
|
+
none (Optional[bool]): Specifies if the axis should be hidden.
|
|
151
|
+
font (Optional[Font]): Font settings for the axis labels.
|
|
152
|
+
major_grid_lines (Optional[bool]): Specifies if major grid lines should be displayed.
|
|
153
|
+
minor_grid_lines (Optional[bool]): Specifies if minor grid lines should be displayed.
|
|
154
|
+
major_unit (Optional[float]): The interval between major grid lines.
|
|
155
|
+
tick_label_skip (Optional[int]): Specifies the number of tick labels to skip between
|
|
156
|
+
each drawn label.
|
|
157
|
+
reverse_order (Optional[bool]): Indicates if the axis order should be reversed.
|
|
158
|
+
secondary (Optional[bool]): Specifies if this is a secondary axis.
|
|
159
|
+
maximum (Optional[float]): The maximum value for the axis.
|
|
160
|
+
minimum (Optional[float]): The minimum value for the axis.
|
|
161
|
+
log_base (Optional[float]): The logarithmic base for the axis scale.
|
|
162
|
+
num_fmt (Optional[ChartCustomNumFmt]): Custom number format for the axis.
|
|
163
|
+
title (Optional[List[RichTextRun]]): The title of the axis.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
none: Optional[bool] = Field(None, serialization_alias='None')
|
|
167
|
+
font: Optional[Font] = Field(Font(), serialization_alias='Font')
|
|
168
|
+
major_grid_lines: Optional[bool] = Field(None, serialization_alias='MajorGridLines')
|
|
169
|
+
minor_grid_lines: Optional[bool] = Field(None, serialization_alias='MinorGridLines')
|
|
170
|
+
major_unit: Optional[float] = Field(None, serialization_alias='MajorUnit')
|
|
171
|
+
tick_label_skip: Optional[int] = Field(None, serialization_alias='TickLabelSkip')
|
|
172
|
+
reverse_order: Optional[bool] = Field(None, serialization_alias='ReverseOrder')
|
|
173
|
+
secondary: Optional[bool] = Field(None, serialization_alias='Secondary')
|
|
174
|
+
maximum: Optional[float] = Field(None, serialization_alias='Maximum')
|
|
175
|
+
minimum: Optional[float] = Field(None, serialization_alias='Minimum')
|
|
176
|
+
log_base: Optional[float] = Field(None, serialization_alias='LogBase')
|
|
177
|
+
num_fmt: Optional[ChartCustomNumFmt] = Field(ChartCustomNumFmt(), serialization_alias='NumFmt')
|
|
178
|
+
title: Optional[List[RichTextRun]] = Field(None, serialization_alias='Title')
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ChartPlotArea(BaseModel):
|
|
182
|
+
"""
|
|
183
|
+
Represents the plot area of a chart.
|
|
184
|
+
|
|
185
|
+
Attributes:
|
|
186
|
+
second_plot_values (Optional[int]): The number of values in a secondary plot
|
|
187
|
+
(Only for pieOfPie and barOfPie chart).
|
|
188
|
+
show_bubble_size (Optional[bool]): Indicates if bubble sizes should be displayed.
|
|
189
|
+
show_cat_name (Optional[bool]): Specifies if category names should be shown.
|
|
190
|
+
show_leader_lines (Optional[bool]): Indicates if leader lines should be shown in
|
|
191
|
+
the data label.
|
|
192
|
+
show_percent (Optional[bool]): Specifies if percentages should be shown in the data label.
|
|
193
|
+
show_ser_name (Optional[bool]): Indicates if series names should be displayed in the
|
|
194
|
+
data label.
|
|
195
|
+
show_val (Optional[bool]): Specifies if values should be shown in the data label.
|
|
196
|
+
fill (Optional[Fill]): Fill settings for the plot area.
|
|
197
|
+
num_fmt (Optional[ChartCustomNumFmt]): Custom number format for the plot area.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
second_plot_values: Optional[int] = Field(None, serialization_alias='SecondPlotValues')
|
|
201
|
+
show_bubble_size: Optional[bool] = Field(None, serialization_alias='ShowBubbleSize')
|
|
202
|
+
show_cat_name: Optional[bool] = Field(None, serialization_alias='ShowCatName')
|
|
203
|
+
show_leader_lines: Optional[bool] = Field(None, serialization_alias='ShowLeaderLines')
|
|
204
|
+
show_percent: Optional[bool] = Field(None, serialization_alias='ShowPercent')
|
|
205
|
+
show_ser_name: Optional[bool] = Field(None, serialization_alias='ShowSerName')
|
|
206
|
+
show_val: Optional[bool] = Field(None, serialization_alias='ShowVal')
|
|
207
|
+
fill: Optional[Fill] = Field(None, serialization_alias='Fill')
|
|
208
|
+
num_fmt: Optional[ChartCustomNumFmt] = Field(ChartCustomNumFmt(), serialization_alias='NumFmt')
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class GraphicOptions(BaseModel):
|
|
212
|
+
"""
|
|
213
|
+
Defines various graphical options for chart elements.
|
|
214
|
+
|
|
215
|
+
Attributes:
|
|
216
|
+
alt_text (Optional[str]): Alternative text for accessibility.
|
|
217
|
+
print_object (Optional[bool]): Indicates if the object should be printed.
|
|
218
|
+
locked (Optional[bool]): Specifies if the object is locked.
|
|
219
|
+
lock_aspect_ratio (Optional[bool]): Indicates if the aspect ratio should be locked.
|
|
220
|
+
auto_fit (Optional[bool]): Specifies if the object should automatically fit its content.
|
|
221
|
+
offset_x (Optional[int]): The horizontal offset of the object.
|
|
222
|
+
offset_y (Optional[int]): The vertical offset of the object.
|
|
223
|
+
scale_x (Optional[float]): The horizontal scale factor.
|
|
224
|
+
scale_y (Optional[float]): The vertical scale factor.
|
|
225
|
+
hyperlink (Optional[str]): A hyperlink associated with the object.
|
|
226
|
+
hyperlink_type (Optional[str]): The type of hyperlink.
|
|
227
|
+
positioning (Optional[str]): The positioning mode for the object.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
alt_text: Optional[str] = Field(None, serialization_alias='AltText')
|
|
231
|
+
print_object: Optional[bool] = Field(None, serialization_alias='PrintObject')
|
|
232
|
+
locked: Optional[bool] = Field(None, serialization_alias='Locked')
|
|
233
|
+
lock_aspect_ratio: Optional[bool] = Field(None, serialization_alias='LockAspectRatio')
|
|
234
|
+
auto_fit: Optional[bool] = Field(None, serialization_alias='AutoFit')
|
|
235
|
+
offset_x: Optional[int] = Field(None, serialization_alias='OffsetX')
|
|
236
|
+
offset_y: Optional[int] = Field(None, serialization_alias='OffsetY')
|
|
237
|
+
scale_x: Optional[float] = Field(None, serialization_alias='ScaleX')
|
|
238
|
+
scale_y: Optional[float] = Field(None, serialization_alias='ScaleY')
|
|
239
|
+
hyperlink: Optional[str] = Field(None, serialization_alias='Hyperlink')
|
|
240
|
+
hyperlink_type: Optional[str] = Field(None, serialization_alias='HyperlinkType')
|
|
241
|
+
positioning: Optional[str] = Field(None, serialization_alias='Positioning')
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class ChartDimension(BaseModel):
|
|
245
|
+
"""
|
|
246
|
+
Specifies the dimensions of a chart.
|
|
247
|
+
|
|
248
|
+
Attributes:
|
|
249
|
+
width (Optional[int]): The width of the chart.
|
|
250
|
+
height (Optional[int]): The height of the chart.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
width: Optional[int] = Field(None, serialization_alias='Width')
|
|
254
|
+
height: Optional[int] = Field(None, serialization_alias='Height')
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class ChartSeries(BaseModel):
|
|
258
|
+
"""
|
|
259
|
+
Represents a data series in a chart.
|
|
260
|
+
|
|
261
|
+
Attributes:
|
|
262
|
+
name (str): The name of the series (Legend).
|
|
263
|
+
categories (str): The categories for the series (X value).
|
|
264
|
+
values (str): The values for the series (Y value).
|
|
265
|
+
sizes (Optional[str]): The sizes for bubble charts.
|
|
266
|
+
fill (Optional[Fill]): Fill settings for the series.
|
|
267
|
+
line (Optional[Line]): Line settings for the series.
|
|
268
|
+
marker (Optional[Marker]): Marker settings for the series.
|
|
269
|
+
data_label_position (Optional[str | ChartDataLabelPosition]): The position
|
|
270
|
+
of data labels for the series.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
name: str = Field(..., serialization_alias='Name')
|
|
274
|
+
categories: str = Field(..., serialization_alias='Categories')
|
|
275
|
+
values: str = Field(..., serialization_alias='Values')
|
|
276
|
+
sizes: Optional[str] = Field(None, serialization_alias='Sizes')
|
|
277
|
+
fill: Optional[Fill] = Field(Fill(), serialization_alias='Fill')
|
|
278
|
+
line: Optional[Line] = Field(Line(), serialization_alias='Line')
|
|
279
|
+
marker: Optional[Marker] = Field(Marker(), serialization_alias='Marker')
|
|
280
|
+
data_label_position: Optional[str | ChartDataLabelPosition] = Field(
|
|
281
|
+
None, serialization_alias='DataLabelPosition'
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
@field_serializer('data_label_position')
|
|
285
|
+
@classmethod
|
|
286
|
+
def data_label_position_validator(cls, label: str | ChartDataLabelPosition | None) -> int:
|
|
287
|
+
if label is None:
|
|
288
|
+
return ChartDataLabelPosition.Unset.value
|
|
289
|
+
if isinstance(label, ChartDataLabelPosition):
|
|
290
|
+
return label.value
|
|
291
|
+
return ChartDataLabelPosition.get_enum(label).value
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class Chart(BaseModel):
|
|
295
|
+
"""
|
|
296
|
+
Defines the configuration for a chart.
|
|
297
|
+
|
|
298
|
+
Attributes:
|
|
299
|
+
chart_type (str | ChartType): The type of chart, such as 'bar', 'line', etc.
|
|
300
|
+
series (List[ChartSeries] | ChartSeries): The data series to be plotted
|
|
301
|
+
in the chart.
|
|
302
|
+
graph_format (Optional[GraphicOptions]): Graphical options for the chart.
|
|
303
|
+
title (Optional[List[RichTextRun]]): The title of the chart.
|
|
304
|
+
legend (Optional[ChartLegend]): The legend settings for the chart.
|
|
305
|
+
dimension (Optional[ChartDimension]): The dimensions of the chart.
|
|
306
|
+
vary_colors (Optional[bool]): Specifies if colors should vary by data point.
|
|
307
|
+
x_axis (Optional[ChartAxis]): The configuration of the X-axis.
|
|
308
|
+
y_axis (Optional[ChartAxis]): The configuration of the Y-axis.
|
|
309
|
+
plot_area (Optional[ChartPlotArea]): The configuration of the plot area.
|
|
310
|
+
fill (Optional[Fill]): The fill settings for the chart.
|
|
311
|
+
border (Optional[Line]): The border settings for the chart.
|
|
312
|
+
show_blanks_as (Optional[str]): Specifies how blanks should be shown in the chart.
|
|
313
|
+
bubble_size (Optional[int]): The size of bubbles in a bubble chart.
|
|
314
|
+
hole_size (Optional[int]): The size of the hole in a doughnut chart.
|
|
315
|
+
order (Optional[int]): The order of the series in the chart.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
chart_type: str | ChartType = Field(..., serialization_alias='Type')
|
|
319
|
+
series: List[ChartSeries] | ChartSeries = Field(None, serialization_alias='Series')
|
|
320
|
+
graph_format: Optional[GraphicOptions] = Field(GraphicOptions(), serialization_alias='Format')
|
|
321
|
+
title: Optional[List[RichTextRun]] = Field(None, serialization_alias='Title')
|
|
322
|
+
legend: Optional[ChartLegend] = Field(ChartLegend(), serialization_alias='Legend')
|
|
323
|
+
dimension: Optional[ChartDimension] = Field(ChartDimension(), serialization_alias='Dimension')
|
|
324
|
+
vary_colors: Optional[bool] = Field(None, serialization_alias='VaryColors')
|
|
325
|
+
x_axis: Optional[ChartAxis] = Field(ChartAxis(), serialization_alias='XAxis')
|
|
326
|
+
y_axis: Optional[ChartAxis] = Field(ChartAxis(), serialization_alias='YAxis')
|
|
327
|
+
plot_area: Optional[ChartPlotArea] = Field(ChartPlotArea(), serialization_alias='PlotArea')
|
|
328
|
+
fill: Optional[Fill] = Field(Fill(), serialization_alias='Fill')
|
|
329
|
+
border: Optional[Line] = Field(Line(), serialization_alias='Border')
|
|
330
|
+
show_blanks_as: Optional[str] = Field(None, serialization_alias='ShowBlanksAs')
|
|
331
|
+
bubble_size: Optional[int] = Field(None, serialization_alias='BubbleSize')
|
|
332
|
+
hole_size: Optional[int] = Field(None, serialization_alias='HoleSize')
|
|
333
|
+
order: Optional[int] = Field(None, serialization_alias='order')
|
|
334
|
+
|
|
335
|
+
@field_serializer('series')
|
|
336
|
+
@classmethod
|
|
337
|
+
def series_serializer(cls, series: List[ChartSeries] | ChartSeries) -> List[ChartSeries]:
|
|
338
|
+
series = [series] if isinstance(series, ChartSeries) else series
|
|
339
|
+
return series
|
|
340
|
+
|
|
341
|
+
@field_serializer('chart_type')
|
|
342
|
+
@classmethod
|
|
343
|
+
def chart_type_validator(cls, chart_type: str | ChartType) -> int:
|
|
344
|
+
if isinstance(chart_type, ChartType):
|
|
345
|
+
return chart_type.value
|
|
346
|
+
return ChartType.get_enum(chart_type).value
|
pyfastexcel/driver.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import ctypes
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import overload
|
|
11
|
+
|
|
12
|
+
import msgspec
|
|
13
|
+
from openpyxl import load_workbook
|
|
14
|
+
from openpyxl_style_writer import CustomStyle
|
|
15
|
+
|
|
16
|
+
from .logformatter import formatter
|
|
17
|
+
from .style import StyleManager
|
|
18
|
+
from .worksheet import WorkSheet
|
|
19
|
+
from .validators import TableFinalValidation
|
|
20
|
+
from ._typing import Writable
|
|
21
|
+
|
|
22
|
+
BASE_DIR = Path(__file__).resolve().parent
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
style_formatter = logging.StreamHandler()
|
|
26
|
+
style_formatter.setFormatter(formatter)
|
|
27
|
+
|
|
28
|
+
logger.addHandler(style_formatter)
|
|
29
|
+
logger.propagate = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ExcelDriver:
|
|
33
|
+
"""
|
|
34
|
+
A driver class to write data to Excel files using custom styles.
|
|
35
|
+
|
|
36
|
+
### Attributes:
|
|
37
|
+
_FILE_PROPS (dict[str, str]): Default file properties for the Excel
|
|
38
|
+
file.
|
|
39
|
+
_PROTECT_ALGORITHM (tuple[str]): Algorithm for the workbook protection
|
|
40
|
+
|
|
41
|
+
### Methods:
|
|
42
|
+
__init__(): Initializes the ExcelDriver.
|
|
43
|
+
_read_lib(lib_path: str): Reads a library for Excel manipulation.
|
|
44
|
+
read_lib_and_create_excel(lib_path: str = None): Reads the library and
|
|
45
|
+
creates the Excel file.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
_FILE_PROPS = {
|
|
49
|
+
'Category': '',
|
|
50
|
+
'ContentStatus': '',
|
|
51
|
+
'Created': '',
|
|
52
|
+
'Creator': 'pyfastexcel',
|
|
53
|
+
'Description': '',
|
|
54
|
+
'Identifier': 'xlsx',
|
|
55
|
+
'Keywords': 'spreadsheet',
|
|
56
|
+
'LastModifiedBy': 'pyfastexcel',
|
|
57
|
+
'Modified': '',
|
|
58
|
+
'Revision': '0',
|
|
59
|
+
'Subject': '',
|
|
60
|
+
'Title': '',
|
|
61
|
+
'Language': 'en-Us',
|
|
62
|
+
'Version': '',
|
|
63
|
+
}
|
|
64
|
+
_PROTECT_ALGORITHM = (
|
|
65
|
+
'XOR',
|
|
66
|
+
'MD4',
|
|
67
|
+
'MD5',
|
|
68
|
+
'SHA-1',
|
|
69
|
+
'SHA-256',
|
|
70
|
+
'SHA-384',
|
|
71
|
+
'SHA-512',
|
|
72
|
+
)
|
|
73
|
+
DEBUG = False
|
|
74
|
+
|
|
75
|
+
def __init__(self, pre_allocate: dict[str, int] = None, plain_data: list[list[str]] = None):
|
|
76
|
+
"""
|
|
77
|
+
Initializes the Workbook with default settings and initializes Sheet1.
|
|
78
|
+
|
|
79
|
+
It initializes the workbook structure with Sheet1 as the default sheet,
|
|
80
|
+
sets default file properties, initializes current sheet and sheet list,
|
|
81
|
+
and initializes dictionaries for workbook and protection settings.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
pre_allocate (dict[str, int], optional): A dictionary containing 'n_rows' and 'n_cols'
|
|
85
|
+
keys specifying the dimensions for pre-allocating data in Sheet1.
|
|
86
|
+
plain_data (list[list[str]], optional): A 2D list of strings representing initial data
|
|
87
|
+
to populate Sheet1.
|
|
88
|
+
"""
|
|
89
|
+
self.workbook = {
|
|
90
|
+
'Sheet1': WorkSheet(pre_allocate=pre_allocate, plain_data=plain_data),
|
|
91
|
+
}
|
|
92
|
+
self.file_props = self._get_default_file_props()
|
|
93
|
+
self.sheet = 'Sheet1'
|
|
94
|
+
self._sheet_list = tuple(['Sheet1'])
|
|
95
|
+
self._dict_wb = {}
|
|
96
|
+
self.protection = {}
|
|
97
|
+
self.style = StyleManager()
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def sheet_list(self):
|
|
101
|
+
return list(self._sheet_list)
|
|
102
|
+
|
|
103
|
+
@overload
|
|
104
|
+
def save(self, file: Writable) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Saves the workbook to a writable object.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
file (Writable): Writable object that has .write() function.
|
|
110
|
+
"""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
@overload
|
|
114
|
+
def save(self, path: str) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Saves the workbook to a file.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
path (str): A path to save the file.
|
|
120
|
+
"""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
def save(self, file_or_path: Writable | str) -> None:
|
|
124
|
+
if not hasattr(self, 'decoded_bytes'):
|
|
125
|
+
self.read_lib_and_create_excel()
|
|
126
|
+
|
|
127
|
+
if isinstance(file_or_path, str):
|
|
128
|
+
with open(file_or_path, 'wb') as file:
|
|
129
|
+
file.write(self.decoded_bytes)
|
|
130
|
+
else:
|
|
131
|
+
file_or_path.write(self.decoded_bytes)
|
|
132
|
+
|
|
133
|
+
def __getitem__(self, key: str) -> WorkSheet:
|
|
134
|
+
return self.workbook[key]
|
|
135
|
+
|
|
136
|
+
def _check_if_sheet_exists(self, sheet_name: str) -> None:
|
|
137
|
+
if sheet_name not in self.sheet_list:
|
|
138
|
+
raise KeyError(f'{sheet_name} Sheet Does Not Exist.')
|
|
139
|
+
|
|
140
|
+
def read_lib_and_create_excel(
|
|
141
|
+
self, lib_path: str = None, ignore_go_panic: bool = True
|
|
142
|
+
) -> bytes:
|
|
143
|
+
"""
|
|
144
|
+
Reads the library and creates the Excel file.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
lib_path (str, optional): The path to the library. Defaults to None.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
bytes: The byte data of the created Excel file.
|
|
151
|
+
"""
|
|
152
|
+
ignore_go_panic = 0 if ignore_go_panic is False else 1
|
|
153
|
+
pyfastexcel = self._read_lib(lib_path)
|
|
154
|
+
self._create_style()
|
|
155
|
+
|
|
156
|
+
# Transfer all WorkSheet Object to the sheet dictionary in the workbook.
|
|
157
|
+
set_group_columns = False
|
|
158
|
+
set_row_columns = False
|
|
159
|
+
use_openpyxl = False
|
|
160
|
+
for sheet in self._sheet_list:
|
|
161
|
+
self._dict_wb[sheet] = self.workbook[sheet]._transfer_to_dict()
|
|
162
|
+
if self.workbook[sheet]._excel_engine == 'openpyxl':
|
|
163
|
+
use_openpyxl = True
|
|
164
|
+
if len(self.workbook[sheet]._grouped_columns_list) != 0:
|
|
165
|
+
set_group_columns = True
|
|
166
|
+
if len(self.workbook[sheet]._grouped_rows_list) != 0:
|
|
167
|
+
set_row_columns = True
|
|
168
|
+
if len(self.workbook[sheet]._table_list) != 0:
|
|
169
|
+
TableFinalValidation(
|
|
170
|
+
data=self.workbook[sheet]._data,
|
|
171
|
+
table_list=self.workbook[sheet]._table_list,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
results = {
|
|
175
|
+
'content': self._dict_wb,
|
|
176
|
+
'file_props': self.file_props,
|
|
177
|
+
'style': self.style._style_map,
|
|
178
|
+
'protection': self.protection,
|
|
179
|
+
'sheet_order': self._sheet_list,
|
|
180
|
+
}
|
|
181
|
+
json_data = msgspec.json.encode(results)
|
|
182
|
+
create_excel = pyfastexcel.Export
|
|
183
|
+
free_pointer = pyfastexcel.FreeCPointer
|
|
184
|
+
free_pointer.argtypes = [ctypes.c_void_p, ctypes.c_int64]
|
|
185
|
+
create_excel.argtypes = [ctypes.c_char_p, ctypes.c_int64]
|
|
186
|
+
create_excel.restype = ctypes.c_void_p
|
|
187
|
+
byte_data = create_excel(json_data, ignore_go_panic)
|
|
188
|
+
self.decoded_bytes = base64.b64decode(ctypes.cast(byte_data, ctypes.c_char_p).value)
|
|
189
|
+
free_pointer(byte_data, 1 if self.DEBUG else 0)
|
|
190
|
+
StyleManager.reset_style_configs()
|
|
191
|
+
|
|
192
|
+
# Due to Streaming API of Excelize can't group column currently
|
|
193
|
+
# So implement this function by openpyxl
|
|
194
|
+
if (set_group_columns or set_row_columns) and use_openpyxl is True:
|
|
195
|
+
logger.info('Using openpyxl to group columns and rows...')
|
|
196
|
+
self.decoded_bytes = self._set_group_columns_and_group_rows()
|
|
197
|
+
|
|
198
|
+
return self.decoded_bytes
|
|
199
|
+
|
|
200
|
+
def _set_group_columns_and_group_rows(self):
|
|
201
|
+
wb = load_workbook(BytesIO(self.decoded_bytes))
|
|
202
|
+
for sheet in self._sheet_list:
|
|
203
|
+
grouped_columns = self.workbook[sheet]._grouped_columns_list
|
|
204
|
+
ws = wb[sheet]
|
|
205
|
+
for col in grouped_columns:
|
|
206
|
+
ws.column_dimensions.group(
|
|
207
|
+
col['start_col'],
|
|
208
|
+
col['end_col'],
|
|
209
|
+
outline_level=col['outline_level'],
|
|
210
|
+
hidden=col['hidden'],
|
|
211
|
+
)
|
|
212
|
+
grouped_rows = self.workbook[sheet]._grouped_rows_list
|
|
213
|
+
for row in grouped_rows:
|
|
214
|
+
ws.row_dimensions.group(
|
|
215
|
+
row['start_row'],
|
|
216
|
+
row['end_row'],
|
|
217
|
+
outline_level=row['outline_level'],
|
|
218
|
+
hidden=row['hidden'],
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Save the workbook to a BytesIO stream
|
|
222
|
+
excel_stream = BytesIO()
|
|
223
|
+
wb.save(excel_stream)
|
|
224
|
+
decoded_bytes = excel_stream.getvalue()
|
|
225
|
+
excel_stream.close()
|
|
226
|
+
return decoded_bytes
|
|
227
|
+
|
|
228
|
+
def _read_lib(self, lib_path: str) -> ctypes.CDLL: # pragma: no cover
|
|
229
|
+
"""
|
|
230
|
+
Reads a shared-library for writing Excel.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
lib_path (str): The path to the library.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
ctypes.CDLL: The library object.
|
|
237
|
+
"""
|
|
238
|
+
if lib_path is None:
|
|
239
|
+
if sys.platform.startswith('linux'):
|
|
240
|
+
lib_path = str(list(BASE_DIR.glob('**/*.so'))[0])
|
|
241
|
+
elif sys.platform.startswith('win32'):
|
|
242
|
+
lib_path = str(list(BASE_DIR.glob('**/*.dll'))[0])
|
|
243
|
+
elif sys.platform.startswith('darwin'):
|
|
244
|
+
lib_path = str(list(BASE_DIR.glob('**/*.dylib'))[0])
|
|
245
|
+
|
|
246
|
+
# On macOS, there is no winmode parameter, so we should not pass it
|
|
247
|
+
if sys.platform.startswith('win32') or sys.platform.startswith('linux'):
|
|
248
|
+
lib = ctypes.CDLL(lib_path, winmode=0)
|
|
249
|
+
else:
|
|
250
|
+
lib = ctypes.CDLL(lib_path)
|
|
251
|
+
return lib
|
|
252
|
+
|
|
253
|
+
def _get_default_file_props(self) -> dict[str, str]:
|
|
254
|
+
now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
255
|
+
file_props = self._FILE_PROPS.copy()
|
|
256
|
+
file_props['Created'] = now
|
|
257
|
+
file_props['Modified'] = now
|
|
258
|
+
return file_props
|
|
259
|
+
|
|
260
|
+
def _get_style_collections(self) -> dict[str, CustomStyle]:
|
|
261
|
+
"""
|
|
262
|
+
Gets collections of custom styles.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
dict[str, CustomStyle]: A dictionary containing custom style
|
|
266
|
+
collections.
|
|
267
|
+
"""
|
|
268
|
+
return {
|
|
269
|
+
attr: getattr(self, attr)
|
|
270
|
+
for attr in dir(self)
|
|
271
|
+
if not callable(getattr(self, attr)) and isinstance(getattr(self, attr), CustomStyle)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
def _create_style(self) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Creates custom styles for the Excel file.
|
|
277
|
+
|
|
278
|
+
This method initializes custom styles for the Excel file based on
|
|
279
|
+
predefined attributes.
|
|
280
|
+
"""
|
|
281
|
+
style_collections = self._get_style_collections()
|
|
282
|
+
self.style._STYLE_NAME_MAP.update({val: key for key, val in style_collections.items()})
|
|
283
|
+
|
|
284
|
+
# Set the CustomStyle from the pre-defined class attributes.
|
|
285
|
+
for key, val in style_collections.items():
|
|
286
|
+
self.style._update_style_map(key, val)
|
|
287
|
+
|
|
288
|
+
# Set the CustomStyle from the REGISTERED method.
|
|
289
|
+
for key, val in self.style.REGISTERED_STYLES.items():
|
|
290
|
+
self.style._update_style_map(key, val)
|