mergeron_extra 2024.739148.7__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.
@@ -0,0 +1,662 @@
1
+ """
2
+ Methods for writing data from Python to fresh Excel workbooks using
3
+ the third-party package, `xlsxwriter`.
4
+
5
+ Includes a flexible system of defining cell formats.
6
+
7
+ NOTES
8
+ -----
9
+
10
+ This module is designed for producing formatted summary output. For
11
+ writing bulk data to Excel, facilities provided in third-party packages
12
+ such as `polars <https://pola.rs/>`_ likely provide better performance.
13
+
14
+ License
15
+ ========
16
+
17
+ Copyright 2017-2023 S. Murthy Kambhampaty
18
+ Licese: MIT
19
+ https://mit-license.org/
20
+
21
+
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from collections.abc import Sequence
27
+ from types import MappingProxyType
28
+ from typing import Any, ClassVar, Literal, TypeAlias, TypedDict, overload
29
+
30
+ import numpy as np
31
+ from aenum import Enum, extend_enum, unique # type: ignore
32
+ from numpy.typing import NDArray
33
+ from xlsxwriter.format import Format # type: ignore
34
+ from xlsxwriter.workbook import Workbook # type: ignore
35
+ from xlsxwriter.worksheet import Worksheet # type: ignore
36
+
37
+ from . import VERSION
38
+
39
+ __version__ = VERSION
40
+
41
+
42
+ XLBorderType: TypeAlias = Literal[
43
+ "none",
44
+ "thin",
45
+ "medium",
46
+ "dashed",
47
+ "dotted",
48
+ "thick",
49
+ "double",
50
+ "hair",
51
+ "medium_dashed",
52
+ "dash_dot",
53
+ "medium_dash_dot",
54
+ "dash_dot_dot",
55
+ "medium_dash_dot_dot",
56
+ "slant_dash_dot",
57
+ True,
58
+ False,
59
+ 0,
60
+ 1,
61
+ 2,
62
+ 3,
63
+ 4,
64
+ 5,
65
+ 6,
66
+ 7,
67
+ 8,
68
+ 9,
69
+ 10,
70
+ 11,
71
+ 12,
72
+ 13,
73
+ ]
74
+
75
+
76
+ class CFmtVal(TypedDict, total=False):
77
+ """Keys for xlsxwriter Format objects.
78
+
79
+ This is a partial list based on formats of interest.
80
+ """
81
+
82
+ font_name: str
83
+ font_size: int
84
+ font_color: str
85
+ align: Literal[
86
+ "left", "center", "right", "center_across", "top", "bottom", "vcenter"
87
+ ]
88
+ text_wrap: bool
89
+ rotation: int # integer, 0-360
90
+ indent: int
91
+ shrink: bool
92
+ bold: bool
93
+ italic: bool
94
+ underline: Literal[
95
+ True,
96
+ False,
97
+ 1,
98
+ 2,
99
+ 33,
100
+ 34,
101
+ "single",
102
+ "double",
103
+ "accountingSingle",
104
+ "accountingDouble",
105
+ ]
106
+ font_strikeout: bool
107
+ font_script: Literal[1, 2]
108
+
109
+ num_format: str
110
+
111
+ pattern: int
112
+ fg_color: str # html color string, no #
113
+ bg_color: str # html color string, no #
114
+
115
+ hidden: bool
116
+ locked: bool
117
+
118
+ border: XLBorderType
119
+ bottom: XLBorderType
120
+ left: XLBorderType
121
+ right: XLBorderType
122
+ top: XLBorderType
123
+ border_color: str # html color string, no #
124
+ bottom_color: str # html color string, no #
125
+ left_color: str # html color string, no #
126
+ right_color: str # html color string, no #
127
+ top_color: str # html color string, no #
128
+
129
+ diag_border: XLBorderType
130
+ diag_border_color: str # html color string, no #
131
+ diag_type: Literal[
132
+ 1, 2, 3, "up", "down", "left", "right", "cross", "diagonalUp", "diagonalDown"
133
+ ]
134
+
135
+
136
+ @unique
137
+ class CFmt(Enum): # type: ignore
138
+ """
139
+ Cell format enums for xlsxwriter Format objects.
140
+
141
+ The enums defined here, or sequences of (any of) them
142
+ and any added with :meth:`CFmt.add_new`, are
143
+ rendered as :code:`xlsxWriter.Workbook.Format` objects
144
+ with :meth:`CFmt.xl_fmt`.
145
+
146
+ NOTES
147
+ -----
148
+
149
+ For more information about xlsxwriter cell formats,
150
+ see, https://xlsxwriter.readthedocs.io/format.html
151
+
152
+ """
153
+
154
+ XL_DEFAULT: ClassVar = MappingProxyType({"font_name": "Calibri", "font_size": 11})
155
+ XL_DEFAULT_2003: ClassVar = MappingProxyType({
156
+ "font_name": "Arial",
157
+ "font_size": 10,
158
+ })
159
+
160
+ A_CTR: ClassVar = MappingProxyType({"align": "center"})
161
+ A_CTR_ACROSS: ClassVar = MappingProxyType({"align": "center_across"})
162
+ A_LEFT: ClassVar = MappingProxyType({"align": "left"})
163
+ A_RIGHT: ClassVar = MappingProxyType({"align": "right"})
164
+ V_TOP: ClassVar = MappingProxyType({"align": "top"})
165
+ V_BOTTOM: ClassVar = MappingProxyType({"align": "bottom"})
166
+ V_CTR: ClassVar = MappingProxyType({"align": "vcenter"})
167
+
168
+ TEXT_WRAP: ClassVar = MappingProxyType({"text_wrap": True})
169
+ TEXT_ROTATE: ClassVar = MappingProxyType({"rotation": 90})
170
+ IND_1: ClassVar = MappingProxyType({"indent": 1})
171
+
172
+ BOLD: ClassVar = MappingProxyType({"bold": True})
173
+ BOLD_ITALIC: ClassVar = MappingProxyType({"bold": True, "italic": True})
174
+ ITALIC: ClassVar = MappingProxyType({"italic": True})
175
+ ULINE: ClassVar = MappingProxyType({"underline": "single"})
176
+ SOUT: ClassVar = MappingProxyType({"font_strikeout": True})
177
+ # Useful with write_rich_text()
178
+ SUPERSCRIPT: ClassVar = MappingProxyType({"font_script": 1})
179
+ SUBSCRIPT: ClassVar = MappingProxyType({"font_script": 2})
180
+
181
+ AREA_NUM: ClassVar = MappingProxyType({"num_format": "0.00000000"})
182
+ DOLLAR_NUM: ClassVar = MappingProxyType({"num_format": "[$$-409]#,##0.00"})
183
+ DT_NUM: ClassVar = MappingProxyType({"num_format": "mm/dd/yyyy"})
184
+ PCT_NUM: ClassVar = MappingProxyType({"num_format": "##0%"})
185
+ PCT2_NUM: ClassVar = MappingProxyType({"num_format": "##0.00%"})
186
+ PCT4_NUM: ClassVar = MappingProxyType({"num_format": "##0.0000%"})
187
+ PCT6_NUM: ClassVar = MappingProxyType({"num_format": "##0.000000%"})
188
+ PCT8_NUM: ClassVar = MappingProxyType({"num_format": "##0.00000000%"})
189
+ QTY_NUM: ClassVar = MappingProxyType({"num_format": "#,##0.0"})
190
+
191
+ BAR_FILL: ClassVar = MappingProxyType({"pattern": 1, "bg_color": "dfeadf"})
192
+ HDR_FILL: ClassVar = MappingProxyType({"pattern": 1, "bg_color": "bfbfbf"})
193
+
194
+ FULL_BORDER: ClassVar = MappingProxyType({"border": 1, "border_color": "000000"})
195
+ BOTTOM_BORDER: ClassVar = MappingProxyType({"bottom": 1, "bottom_color": "000000"})
196
+ LEFT_BORDER: ClassVar = MappingProxyType({"left": 1, "left_color": "000000"})
197
+ RIGHT_BORDER: ClassVar = MappingProxyType({"right": 1, "right_color": "000000"})
198
+ TOP_BORDER: ClassVar = MappingProxyType({"top": 1, "top_color": "000000"})
199
+ HDR_BORDER: ClassVar = MappingProxyType(TOP_BORDER | BOTTOM_BORDER)
200
+
201
+ @classmethod
202
+ def add_new(cls, _fmt_name: str, _xlsx_fmt_dict: CFmtVal, /) -> CFmt:
203
+ """
204
+ Add new :class:`CFmt` object to instance.
205
+
206
+ Parameters
207
+ ----------
208
+ _fmt_name
209
+ Name of new member to be added to :class:`CFmt`
210
+ _xlsx_fmt_dict
211
+ Any valid argument to :code:`xlsxwriter.Workbook.add_format()`, or union of
212
+ same with the value of one or more :class:`CFmt` objects, e.g.,
213
+ :code:`CFmt.HDR_BORDER.value | CFmt.HDR_FILL.value` or
214
+ :code:`CFmt.HDR_BORDER.value | {"pattern": 1, "bg_color": "f2f2f2"}`
215
+
216
+ Returns
217
+ -------
218
+ None
219
+
220
+ """
221
+
222
+ return extend_enum(cls, _fmt_name, MappingProxyType(_xlsx_fmt_dict)) # type: ignore
223
+
224
+ @classmethod
225
+ def ensure_cell_format_spec_tuple(
226
+ cls, _cell_format: Sequence[CFmt | Sequence[CFmt]], /
227
+ ) -> bool:
228
+ """
229
+ Test that a given format specification is a tuple of :class:`CFmt` enums
230
+
231
+ Parameters
232
+ ----------
233
+ _cell_format
234
+ Format specification
235
+
236
+ Raises
237
+ ------
238
+ ValueError
239
+ If format specification is not a sequence of (sequences of)
240
+ :class:`CFmt` enums
241
+
242
+ Returns
243
+ -------
244
+ True if format specification passes, else False
245
+
246
+ """
247
+
248
+ for _cf in _cell_format:
249
+ if isinstance(_cf, tuple):
250
+ cls.ensure_cell_format_spec_tuple(_cf)
251
+
252
+ if not (isinstance(_cf, CFmt),):
253
+ raise ValueError(
254
+ "Improperly specified format tuple for writing array."
255
+ " Must be tuple of :class:`CFmt` enums."
256
+ )
257
+
258
+ return True
259
+
260
+ @classmethod
261
+ def xl_fmt(
262
+ cls,
263
+ _xl_book: Workbook,
264
+ _cell_format: Sequence[CFmt | Sequence[CFmt]] | CFmt | None,
265
+ /,
266
+ ) -> Format:
267
+ """
268
+ Return :code:`xlsxwriter` :code:`Format` object given a :class:`CFmt` enum, or tuple thereof.
269
+
270
+ Parameters
271
+ ----------
272
+ _xl_book
273
+ :code:`xlsxwriter.Workbook` object
274
+
275
+ _cell_format
276
+ :class:`CFmt` enum object, or tuple thereof
277
+
278
+ Raises
279
+ ------
280
+ ValueError
281
+ If format specification is not one of None, a :class:`CFmt` enum, or
282
+ a :code:`Format` object
283
+
284
+ Returns
285
+ -------
286
+ :code:`xlsxwriter` :code:`Format` object
287
+
288
+ """
289
+
290
+ if isinstance(_cell_format, Format):
291
+ return _cell_format
292
+ elif _cell_format is None:
293
+ return _xl_book.add_format(CFmt.XL_DEFAULT.value)
294
+
295
+ _cell_format_dict: CFmtVal = {}
296
+ if isinstance(_cell_format, Sequence):
297
+ cls.ensure_cell_format_spec_tuple(_cell_format)
298
+ for _cf in _cell_format:
299
+ if isinstance(_cf, Sequence):
300
+ for _cfi in _cf:
301
+ _cell_format_dict |= _cfi.value
302
+ else:
303
+ _cell_format_dict |= _cf.value
304
+ elif isinstance(_cell_format, CFmt):
305
+ _cell_format_dict = _cell_format.value
306
+ else:
307
+ raise ValueError("Improperly specified format specification.")
308
+
309
+ return _xl_book.add_format(_cell_format_dict)
310
+
311
+
312
+ def write_header(
313
+ _xl_sheet: Worksheet,
314
+ /,
315
+ *,
316
+ center_header: str | None = None,
317
+ left_header: str | None = None,
318
+ right_header: str | None = None,
319
+ ) -> None:
320
+ """Write header text to given worksheet.
321
+
322
+ Parameters
323
+ ----------
324
+ _xl_sheet
325
+ Worksheet object
326
+ center_header
327
+ Text for center header
328
+ left_header
329
+ Text for left header
330
+ right_header
331
+ Text for right header
332
+
333
+ Raises
334
+ ------
335
+ ValueError
336
+ Must specify at least one header
337
+
338
+ Returns
339
+ -------
340
+ None
341
+ """
342
+ if any((center_header, left_header, right_header)):
343
+ _xl_sheet.set_header(
344
+ "".join([
345
+ f"&L{left_header}" if left_header else "",
346
+ f"&C{center_header}" if center_header else "",
347
+ f"&R{right_header}" if right_header else "",
348
+ ])
349
+ )
350
+
351
+ else:
352
+ raise ValueError("must specify at least one header")
353
+
354
+
355
+ def write_footer(
356
+ _xl_sheet: Worksheet,
357
+ /,
358
+ *,
359
+ center_footer: str | None = None,
360
+ left_footer: str | None = None,
361
+ right_footer: str | None = None,
362
+ ) -> None:
363
+ """Write footer text to given worksheet.
364
+
365
+ Parameters
366
+ ----------
367
+ _xl_sheet
368
+ Worksheet object
369
+ center_footer
370
+ Text for center footer
371
+ left_footer
372
+ Text for left footer
373
+ right_footer
374
+ Text for right footer
375
+
376
+ Raises
377
+ ------
378
+ ValueError
379
+ Must specify at least one footer
380
+
381
+ Returns
382
+ -------
383
+ None
384
+ """
385
+
386
+ if any((center_footer, left_footer, right_footer)):
387
+ _xl_sheet.set_footer(
388
+ "".join([
389
+ f"&L{left_footer}" if left_footer else "",
390
+ f"&C{center_footer}" if center_footer else "",
391
+ f"&R{right_footer}" if right_footer else "",
392
+ ])
393
+ )
394
+
395
+ else:
396
+ raise ValueError("must specify at least one footer")
397
+
398
+
399
+ def array_to_sheet(
400
+ _xl_book: Workbook,
401
+ _xl_sheet: Worksheet,
402
+ _data_table: Sequence[Any] | NDArray[Any],
403
+ _row_id: int,
404
+ _col_id: int = 0,
405
+ /,
406
+ *,
407
+ cell_format: Sequence[CFmt | Sequence[CFmt]] | CFmt | None = None,
408
+ green_bar_flag: bool = True,
409
+ ragged_flag: bool = True,
410
+ ) -> tuple[int, int]:
411
+ """
412
+ Write a 2-D array to a worksheet.
413
+
414
+ The given array is required be a two-dimensional array, whether
415
+ a nested list, nested tuple, or a 2-D numpy ndarray. The array is assumed
416
+ to be ragged by default, i.e. not all rows are the same length, and some
417
+ cells may contain lists, etc. For rectangular arrays, set `ragged_flag` to
418
+ false if you wish to provide a format tuple with distinct formats for each
419
+ column in the rectangular array.
420
+
421
+
422
+ Parameters
423
+ ----------
424
+ _xl_book
425
+ Workbook object
426
+
427
+ _xl_sheet
428
+ Worksheet object to which to write the give array
429
+
430
+ _data_table
431
+ Array to be written
432
+
433
+ _row_id
434
+ Row number of top left corner of range to write to
435
+
436
+ _col_id
437
+ Column number of top left corner of range to write to
438
+
439
+ cell_format
440
+ Format specification for range to be written
441
+
442
+ green_bar_flag
443
+ Whether to highlight alternating rows as in green bar paper
444
+
445
+ ragged_flag
446
+ Whether to write ragged array, i.e. rows not all the same length
447
+ or not all cells are scalar-valued
448
+
449
+
450
+ Raises
451
+ ------
452
+ ValueError
453
+ If array is not two-dimensional
454
+
455
+ ValueError
456
+ If ragged_flag is False and array is not rectangular
457
+
458
+ ValueError
459
+ If array is not rectangular and cell_format is a Sequence
460
+
461
+ ValueError
462
+ If array is rectangular but length of format tuple does not
463
+ match row-length
464
+
465
+
466
+ Returns
467
+ -------
468
+ Tuple giving address of cell at right below and after range written
469
+
470
+
471
+ Notes
472
+ -----
473
+
474
+ The keyword argument cell_format may be passed a tuple of :class:`CFmt` enums,
475
+ if, and only if, ragged_flag is False. If cell_format is a tuple, it must
476
+ have length equal to the number of cells in each row of the passed array.
477
+ Further, members of cell_format must each be a :class:`CFmt` enum or a
478
+ tuple of :class:`CFmt` enums; in other words, :meth:`CFmt.ensure_cell_format_spec_tuple`
479
+ must return True for any tuple `_c` passed as `cell_format`.
480
+
481
+ """
482
+
483
+ if not ragged_flag:
484
+ try:
485
+ if np.ndim(_data_table) != 2:
486
+ raise ValueError("Given array must be two-dimensional.")
487
+ except ValueError as _err:
488
+ raise ValueError(
489
+ "Given array must be rectangular and homogenous, with scalar members."
490
+ " Alternatively, try with ragged_flag=True."
491
+ )
492
+ raise _err
493
+ elif not (
494
+ isinstance(_data_table, Sequence | np.ndarray)
495
+ and isinstance(_data_table[0], Sequence | np.ndarray)
496
+ ):
497
+ raise ValueError("Given array must be two-dimensional array.")
498
+
499
+ # Get the array dimensions and row and column numbers for Excel
500
+ _num_rows = len(_data_table)
501
+ _bottom_row_id = _row_id + _num_rows
502
+ _num_cols = len(_data_table[0])
503
+ _right_column_id = _col_id + _num_cols
504
+
505
+ _cell_format: Sequence[CFmt | Sequence[CFmt]]
506
+ if isinstance(cell_format, Sequence):
507
+ if _num_rows > 1 and ragged_flag:
508
+ raise ValueError(
509
+ "It is not clear whether the sequence of formats applies to all cells,"
510
+ " or to each cell respectively. Please provide a single-valued cell_format."
511
+ " Alternatively, you can iterate over the array using scalar_to_sheet()."
512
+ )
513
+ elif not len(cell_format) == len(_data_table[0]):
514
+ raise ValueError("Format tuple does not match data in length.")
515
+ CFmt.ensure_cell_format_spec_tuple(cell_format)
516
+ _cell_format = cell_format
517
+ elif isinstance(cell_format, CFmt):
518
+ _cell_format = (cell_format,) * len(_data_table[0])
519
+ else:
520
+ _cell_format = (CFmt.XL_DEFAULT,) * len(_data_table[0])
521
+
522
+ # construct vector of xlslwrter.format.Format objects
523
+ _wbk_formats = tuple(CFmt.xl_fmt(_xl_book, _cf) for _cf in _cell_format)
524
+ _wbk_formats_greened = _wbk_formats
525
+ if _num_rows > 1:
526
+ _wbk_formats_greened = (
527
+ tuple(
528
+ CFmt.xl_fmt(
529
+ _xl_book,
530
+ (*_cf, CFmt.BAR_FILL)
531
+ if isinstance(_cf, Sequence)
532
+ else (_cf, CFmt.BAR_FILL),
533
+ )
534
+ for _cf in _cell_format
535
+ )
536
+ if green_bar_flag
537
+ else _wbk_formats
538
+ )
539
+
540
+ for _ri, _rv in enumerate(_data_table):
541
+ _wbk_fmt_tuple = _wbk_formats_greened if _ri % 2 else _wbk_formats
542
+ for _ci, _cv in enumerate(_rv):
543
+ _cf = _wbk_fmt_tuple[_ci]
544
+ scalar_to_sheet(_xl_book, _xl_sheet, _row_id + _ri, _col_id + _ci, _cv, _cf)
545
+
546
+ _right_column_id = (
547
+ _col_id + len(_rv) if len(_rv) > _num_cols else _right_column_id
548
+ )
549
+
550
+ return _bottom_row_id, _right_column_id
551
+
552
+
553
+ @overload
554
+ def scalar_to_sheet(
555
+ _xl_book: Workbook,
556
+ _xl_sheet: Worksheet,
557
+ _address0: str,
558
+ _value: Any,
559
+ _format: CFmt | Sequence[CFmt | Sequence[CFmt]] | None,
560
+ /,
561
+ ) -> None: ...
562
+
563
+
564
+ @overload
565
+ def scalar_to_sheet(
566
+ _xl_book: Workbook,
567
+ _xl_sheet: Worksheet,
568
+ _address0: int,
569
+ _address1: int,
570
+ _value: Any,
571
+ _format: CFmt | Sequence[CFmt | Sequence[CFmt]] | None,
572
+ /,
573
+ ) -> None: ...
574
+
575
+
576
+ def scalar_to_sheet(
577
+ _xl_book: Workbook, _xl_sheet: Worksheet, /, *_s2s_args: Any
578
+ ) -> None:
579
+ """
580
+ Write to a single cell in a worksheet.
581
+
582
+ Parameters
583
+ ----------
584
+ _xl_book
585
+ Workbook object for defining formats, and writing data
586
+
587
+ _xl_sheet
588
+ Worksheet object to which to write the given scalar
589
+
590
+ _cell_addr
591
+ An Excel cell address string in 'A1' format
592
+
593
+ _address0
594
+ Index-0 row number of destintaion cell
595
+
596
+ _address1
597
+ Index-0 column number of destintaion cell
598
+
599
+ _value
600
+ Value to write
601
+
602
+ _format
603
+ Member of :class:`CFmt`, or tuple thereof
604
+
605
+ Raises
606
+ ------
607
+ ValueError
608
+ If too many or too few arguments
609
+ ValueError
610
+ If incorrect/incomplete specification for Excel cell data
611
+
612
+ Returns
613
+ -------
614
+ None
615
+
616
+ Notes
617
+ -----
618
+ For more information on xlsxwriter cell-address notation, see:
619
+ https://xlsxwriter.readthedocs.io/working_with_cell_notation.html
620
+
621
+ """
622
+
623
+ _address: tuple[str] | tuple[int, int]
624
+ _value: Any
625
+ _format: CFmt | Sequence[CFmt | Sequence[CFmt]] | None
626
+
627
+ if isinstance(_s2s_args[0], str):
628
+ if len(_s2s_args) not in (2, 3):
629
+ raise ValueError("Incorrect number of arguments.")
630
+ _address = (_s2s_args[0],)
631
+ _value = _s2s_args[1]
632
+ _format = _s2s_args[2] if len(_s2s_args) == 3 else None
633
+ elif isinstance(_s2s_args[0], int):
634
+ if not isinstance(_s2s_args[1], int) or len(_s2s_args) not in (3, 4):
635
+ print(repr(_s2s_args))
636
+ raise ValueError("Incorrect/incomplete specification for Excel cell data.")
637
+ _address = _s2s_args[:2]
638
+ _value = _s2s_args[2]
639
+ _format = _s2s_args[3] if len(_s2s_args) == 4 else None
640
+ else:
641
+ raise ValueError("Incorrect/incomplete specification for Excel cell data.")
642
+
643
+ _write_args = (
644
+ *_address,
645
+ (
646
+ repr(_value)
647
+ if np.ndim(_value) or _value in (np.inf, -np.inf, np.nan)
648
+ else _value
649
+ ),
650
+ )
651
+ _write_args += (CFmt.xl_fmt(_xl_book, _format),) if _format else ()
652
+
653
+ if _value is None or _value == "":
654
+ _xl_sheet.write_blank(*_write_args)
655
+ elif (
656
+ isinstance(_value, str)
657
+ or np.ndim(_value)
658
+ or _value in (np.inf, -np.inf, np.nan)
659
+ ):
660
+ _xl_sheet.write_string(*_write_args)
661
+ else:
662
+ _xl_sheet.write(*_write_args)