mergeron 2024.739097.4__py3-none-any.whl → 2024.739099.0__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.

Potentially problematic release.


This version of mergeron might be problematic. Click here for more details.

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