toolsos 0.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.
@@ -0,0 +1,32 @@
1
+ import plotly.express as px
2
+
3
+ from .styler import BaseStyle
4
+
5
+ basestyle = BaseStyle()
6
+
7
+
8
+ def pie(
9
+ data,
10
+ names,
11
+ values,
12
+ hole: float = 0.4,
13
+ width=750,
14
+ height=490,
15
+ text_format: str = None,
16
+ **kwargs,
17
+ ):
18
+ fig = px.pie(
19
+ data_frame=data,
20
+ names=names,
21
+ values=values,
22
+ width=width,
23
+ height=height,
24
+ hole=hole,
25
+ template=BaseStyle().get_base_template(),
26
+ **kwargs,
27
+ )
28
+
29
+ if text_format:
30
+ fig.update_traces(texttemplate=text_format)
31
+
32
+ return fig
@@ -0,0 +1,97 @@
1
+ import json
2
+
3
+ import plotly.graph_objects as go
4
+ import requests
5
+ from requests import ConnectionError
6
+
7
+
8
+ class BaseStyle:
9
+ style_url = (
10
+ "https://raw.githubusercontent.com/jbosga-ams/oistyle/main/base_style.json"
11
+ )
12
+
13
+ def __init__(self):
14
+ self.grab_styling()
15
+
16
+ def grab_styling(self, style_path: str = None):
17
+ if not style_path:
18
+ try:
19
+ res = requests.get(self.style_url).json()
20
+ except ConnectionError:
21
+ print("Failed grabbing basestyle from the interwebs")
22
+ # Add option to manually provide json file
23
+ else:
24
+ res = json.loads()
25
+
26
+ for k, v in res.items():
27
+ setattr(self, k, v)
28
+
29
+ def _get_axis_format(self):
30
+ self.gridline_color = "#dbdbdb" # Jorren vragen om deze aan te passen
31
+
32
+ return {
33
+ "zerolinecolor": self.gridline_color,
34
+ "gridcolor": self.gridline_color,
35
+ "gridwidth": self.gridline_width,
36
+ "showline": True,
37
+ "linewidth": self.gridline_width,
38
+ "linecolor": self.gridline_color,
39
+ # "mirror": True,
40
+ "showgrid": False,
41
+ }
42
+
43
+ def _get_base_template_layout(self):
44
+ return go.layout.Template(
45
+ layout={
46
+ "font": {"family": self.font, "size": self.font_size},
47
+ "plot_bgcolor": self.plot_bgcolor,
48
+ "colorway": self.colors["darkblue_lightblue_gradient_5"],
49
+ "separators": ",", # Jorren vragen om deze toe te voegen
50
+ }
51
+ )
52
+
53
+ def get_base_template(
54
+ self, graph_type: str = None, orientation: str = None, colors: str = None
55
+ ):
56
+ """[summary]
57
+
58
+ Args:
59
+ graph_type (str, optional): Pick 'bar', 'line' or 'bar'. Defaults to None.
60
+ orientation (str, optional): [description]. Pick horizontal ('h') or vertical 'v'. Defaults to None.
61
+ colors (str, optional): Set basecolors. Defaults to None.
62
+
63
+ Raises:
64
+ ValueError: [description]
65
+
66
+ Returns:
67
+ [type]: [description]
68
+ """
69
+ base_template = self._get_base_template_layout()
70
+ axis_format = self._get_axis_format()
71
+
72
+ if graph_type == "bar":
73
+ if orientation in ["v", "vertical"]:
74
+ base_template.layout.xaxis.update(axis_format)
75
+ base_template.layout.yaxis.update(zeroline=False)
76
+ elif orientation in ["h", "horizontal"]:
77
+ base_template.layout.yaxis.update(axis_format)
78
+ base_template.layout.xaxis.update(zeroline=False)
79
+ else:
80
+ raise ValueError(
81
+ "Orientation ('v'/'vertical' or 'h'/'horizontal') should be supplied with graph_type=='bar'"
82
+ )
83
+
84
+ elif graph_type == "line":
85
+ base_template.layout.xaxis.update(axis_format)
86
+
87
+ if colors:
88
+ base_template.layout.update({"colorway": colors})
89
+
90
+ return base_template
91
+
92
+ def get_ois_colors(self, colorscale):
93
+ colorscale = self.colors.get(colorscale, [])
94
+ if not colorscale:
95
+ raise Exception(f"Kies uit {self.colors.keys()}")
96
+
97
+ return colorscale
File without changes
@@ -0,0 +1,35 @@
1
+ STYLE_OLD = {
2
+ "blue_white": {
3
+ "fill": {"fill_type": "solid", "fgColor": "00a0e6"},
4
+ "font": {"color": "FFFFFF", "bold": False},
5
+ },
6
+ "light_blue": {
7
+ "fill": {"fill_type": "solid", "fgColor": "B1D9F5"},
8
+ "font": {"bold": False},
9
+ },
10
+ "calibri": {"font": {"name": "Calibri", "size": 9}},
11
+ "blue_border_bottom": {
12
+ "border_bottom": {"color": "00a0e6", "border_style": "medium"}
13
+ },
14
+ "left_align": {"alignment": {"horizontal": "left"}, "font": {"bold": False}},
15
+ "right_align": {"alignment": {"horizontal": "right"}},
16
+ "title_bold": {"font": {"bold": True, "name": "Calibri", "size": 9}},
17
+ }
18
+
19
+ STYLE_NEW = {
20
+ "blue_white": {
21
+ "fill": {"fill_type": "solid", "fgColor": "004699"},
22
+ "font": {"color": "FFFFFF", "bold": True},
23
+ },
24
+ "light_blue": {
25
+ "fill": {"fill_type": "solid", "fgColor": "b8bcdd"},
26
+ "font": {"bold": True},
27
+ },
28
+ "calibri": {"font": {"name": "Calibri", "size": 9}},
29
+ "blue_border_bottom": {
30
+ "border_bottom": {"color": "004699", "border_style": "medium"}
31
+ },
32
+ "left_align": {"alignment": {"horizontal": "left"}, "font": {"bold": True}},
33
+ "right_align": {"alignment": {"horizontal": "right"}},
34
+ "title_bold": {"font": {"bold": True, "name": "Calibri", "size": 9}},
35
+ }
@@ -0,0 +1,508 @@
1
+ import functools
2
+ import json
3
+ import re
4
+ from typing import Any, Callable, Dict
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ from openpyxl import Workbook
9
+ from openpyxl.styles import Alignment, Border, Font, PatternFill, Protection, Side
10
+ from openpyxl.utils import get_column_letter
11
+
12
+ Fmt = list[list[dict[str, Any]]]
13
+ Mapping = Dict[str, Dict[str, str | int | bool]]
14
+
15
+ LOOKUP: dict[str, Callable] = {
16
+ "font": Font,
17
+ "fill": PatternFill,
18
+ "alignment": Alignment,
19
+ "border": Border,
20
+ "protection": Protection,
21
+ "side": Side,
22
+ "border": Border,
23
+ }
24
+
25
+
26
+ def set_global_style(style: str) -> None:
27
+ global STYLES
28
+ if style == "old":
29
+ from table_styles import STYLE_OLD
30
+
31
+ STYLES = STYLE_OLD
32
+ elif style == "new":
33
+ from table_styles import STYLE_NEW
34
+
35
+ STYLES = STYLE_NEW
36
+
37
+
38
+ # We are currently in the process of switching to the new `huisstijl`. Therefore the
39
+ # styling is stored in a json. After loading is treated as a constant
40
+ # STYLES = get_table_style_from_json()
41
+
42
+ LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
43
+
44
+
45
+ def df_to_array(df: pd.DataFrame) -> np.ndarray:
46
+ """Turn dataframe into array that includes column names as the first row
47
+
48
+ Args:
49
+ df (pd.DataFrame): Dataframe to be turned into an array
50
+
51
+ Returns:
52
+ np.array: Array that includes the data and the column names as the first row
53
+ """
54
+ return np.vstack([df.columns, df.to_numpy()])
55
+
56
+
57
+ def get_fmt_table(arr: np.ndarray) -> Fmt:
58
+ """Create nested list with dictionary inside that is the same size as the original
59
+ dataframe including column names
60
+
61
+ Args:
62
+ df (pd.DataFrame): Dataframe to be written to excel
63
+
64
+ Returns:
65
+ Fmt: Return empty nest list that will later be used to store formatting info
66
+ """
67
+ fmt = []
68
+ for _ in range(arr.shape[0] + 1):
69
+ row: list = []
70
+ for _ in range(arr.shape[1]):
71
+ row.append({})
72
+ fmt.append(row)
73
+
74
+ return fmt
75
+
76
+
77
+ def update_format(fmt: Fmt, row_idx: int, col_idx: int, mapping: Mapping) -> Fmt:
78
+ """Update the cell containing the formatting info
79
+
80
+ Args:
81
+ fmt (Fmt): nested list containing the formatting info
82
+ row_idx (int): row index
83
+ col_idx (int): column index
84
+ mapping (Mapping): formatting info
85
+
86
+ Returns:
87
+ Fmt: nested list containing the updated formatting info
88
+ """
89
+ for fmt_type, args in mapping.items():
90
+ cell = fmt[row_idx][col_idx].get(fmt_type)
91
+
92
+ if not cell:
93
+ fmt[row_idx][col_idx][fmt_type] = args
94
+ else:
95
+ fmt[row_idx][col_idx][fmt_type] = cell | args
96
+
97
+ return fmt
98
+
99
+
100
+ def set_style_all(fmt: Fmt, mapping: Mapping) -> Fmt:
101
+ """Set the formatting for all cells
102
+
103
+ Args:
104
+ fmt (Fmt): nested list containing the formatting info
105
+ mapping (Mapping): formatting info
106
+
107
+ Returns:
108
+ Fmt: nested list containing the formatting info
109
+ """
110
+ for row_idx, row in enumerate(fmt):
111
+ for col_idx, _ in enumerate(row):
112
+ update_format(fmt, row_idx, col_idx, mapping)
113
+
114
+ return fmt
115
+
116
+
117
+ def set_style_row(
118
+ fmt: Fmt,
119
+ row_idxs: int | list,
120
+ mapping: Mapping,
121
+ exlude_col_ids: int | list | None = None,
122
+ ) -> Fmt:
123
+ """Set the formatting on a row
124
+
125
+ Args:
126
+ fmt (Fmt): nested list containing the formatting info
127
+ row_idxs (int | list): The indices of the rows to be updated
128
+ mapping (Mapping): formatting info
129
+ exlude_col_ids (int | list | None, optional): Indices of the cols to be excluded
130
+ when setting formatting for a row. Defaults to None.
131
+
132
+ Returns:
133
+ Fmt: nested list containing the formatting info
134
+ """
135
+ if isinstance(row_idxs, int):
136
+ row_idxs = [row_idxs]
137
+
138
+ if exlude_col_ids is not None:
139
+ if isinstance(exlude_col_ids, int):
140
+ exlude_col_ids = [exlude_col_ids]
141
+ else:
142
+ exlude_col_ids = []
143
+
144
+ for row_idx in row_idxs:
145
+ for col_idx, _ in enumerate(fmt[row_idx]):
146
+ if col_idx not in exlude_col_ids:
147
+ update_format(fmt, row_idx, col_idx, mapping)
148
+
149
+ return fmt
150
+
151
+
152
+ def set_style_col(
153
+ fmt: Fmt,
154
+ col_idxs: int | list,
155
+ mapping: Mapping,
156
+ exlude_row_ids: int | list | None = None,
157
+ ) -> Fmt:
158
+ """Set the formatting on a row
159
+
160
+ Args:
161
+ fmt (Fmt): nested list containing the formatting info
162
+ row_idxs (int | list): The indices of the rows to be updated
163
+ mapping (Mapping): formatting info
164
+ exlude_row_ids (int | list | None, optional): Indices of the rows to be excluded
165
+ when setting formatting for a col. Defaults to None.
166
+
167
+ Returns:
168
+ Fmt: nested list containing the formatting info
169
+ """
170
+ if isinstance(col_idxs, int):
171
+ col_idxs = [col_idxs]
172
+
173
+ if exlude_row_ids is not None:
174
+ if isinstance(exlude_row_ids, int):
175
+ exlude_row_ids = [exlude_row_ids]
176
+ else:
177
+ exlude_row_ids = []
178
+
179
+ for col_idx in col_idxs:
180
+ for row_idx, _ in enumerate(fmt):
181
+ if row_idx not in exlude_row_ids:
182
+
183
+ update_format(fmt, row_idx, col_idx, mapping)
184
+
185
+ return fmt
186
+
187
+
188
+ def excel_style(row: int, col: int) -> str:
189
+ """Convert given row and column number to an Excel-style cell name."""
190
+ result: list = []
191
+ while col:
192
+ col, rem = divmod(col - 1, 26)
193
+ result[:0] = LETTERS[rem]
194
+ return "".join(result) + str(row)
195
+
196
+
197
+ def get_cols_id_with_pattern(df: pd.DataFrame, pattern: str) -> list[int]:
198
+ """Get columns indices from columns matching a regex pattern
199
+
200
+ Args:
201
+ df (pd.DataFrame): Input dataframe
202
+ pattern (str): regex pattern to get columns indices when matching
203
+
204
+ Returns:
205
+ list[int]: list with column indices matching pattern
206
+ """
207
+ return [idx for idx, col in enumerate(df.columns) if re.findall(pattern, col)]
208
+
209
+
210
+ def get_string_cols_ids(df: pd.DataFrame) -> list[int]:
211
+ """Get column indices of string columns
212
+
213
+ Args:
214
+ df (pd.DataFrame): Input dataframe
215
+
216
+ Returns:
217
+ list[int]: list with column indices of string columns
218
+ """
219
+ return [i for i, dtype in enumerate(df.dtypes) if dtype == "O"]
220
+
221
+
222
+ def get_numeric_col_ids(df: pd.DataFrame) -> list[int]:
223
+ """Get column indices of numeric columns
224
+
225
+ Args:
226
+ df (pd.DataFrame): Input dataframe
227
+
228
+ Returns:
229
+ list[int]: list with column indices of numeric columns
230
+ """
231
+ num_cols = df.select_dtypes("number").columns
232
+ return [i for i, col in enumerate(df.columns) if col in num_cols]
233
+
234
+
235
+ def cell_formatting(
236
+ arr: np.ndarray,
237
+ default_format: Mapping | None = None,
238
+ blue_row_ids: int | list | None = None,
239
+ light_blue_row_ids: int | list | None = None,
240
+ light_blue_col_ids: int | list | None = None,
241
+ left_align_ids: int | list | None = None,
242
+ right_align_ids: int | list | None = None,
243
+ perc_col_ids: int | list | None = None,
244
+ perc_col_format: str | None = None,
245
+ blue_border: bool | None = None,
246
+ number_format: str | None = None,
247
+ ):
248
+ """Function to create the nested list with the shape of the input data (including columns)
249
+ containing dictionaries with the formatting
250
+
251
+ Args:
252
+ arr (np.ndarray): array representing the data
253
+ default_format (Mapping | None, optional): Default format applied to all cells. Defaults to None.
254
+ blue_row_ids (int | list | None, optional): The ids of the rows to be colored blue. Defaults to None.
255
+ light_blue_row_ids (int | list | None, optional): _description_. Defaults to None.
256
+ light_blue_col_ids (int | list | None, optional): _description_. Defaults to None.
257
+ left_align_ids (int | list | None, optional): _description_. Defaults to None.
258
+ right_align_ids (int | list | None, optional): _description_. Defaults to None.
259
+ perc_col_ids (int | list | None, optional): _description_. Defaults to None.
260
+ perc_col_format (str | None, optional): _description_. Defaults to None.
261
+ blue_border (bool | None, optional): _description_. Defaults to None.
262
+ number_format (str | None, optional): _description_. Defaults to None.
263
+
264
+ Returns:
265
+ _type_: _description_
266
+ """
267
+ fmt = get_fmt_table(arr)
268
+
269
+ if default_format:
270
+ fmt = set_style_all(fmt, default_format)
271
+
272
+ if number_format:
273
+ fmt = set_style_all(fmt, {"number_format": {"format": number_format}})
274
+
275
+ if blue_row_ids:
276
+ fmt = set_style_row(fmt, blue_row_ids, STYLES["blue_white"])
277
+
278
+ if light_blue_row_ids:
279
+ fmt = set_style_row(fmt, light_blue_row_ids, STYLES["light_blue"])
280
+
281
+ if light_blue_col_ids:
282
+ fmt = set_style_col(fmt, light_blue_col_ids, STYLES["light_blue"], blue_row_ids)
283
+
284
+ if left_align_ids:
285
+ fmt = set_style_col(fmt, left_align_ids, STYLES["left_align"])
286
+
287
+ if right_align_ids:
288
+ fmt = set_style_col(fmt, right_align_ids, STYLES["right_align"])
289
+
290
+ if perc_col_ids:
291
+ if not perc_col_format:
292
+ perc_col_format = "0.0%"
293
+ fmt = set_style_col(
294
+ fmt, perc_col_ids, {"number_format": {"format": perc_col_format}}
295
+ )
296
+
297
+ if blue_border:
298
+ fmt = set_style_row(fmt, arr.shape[0] - 1, STYLES["blue_border_bottom"])
299
+
300
+ return fmt
301
+
302
+
303
+ def write_worksheet(
304
+ ws: Any,
305
+ arr: np.ndarray,
306
+ fmt: Fmt,
307
+ title: str | None = None,
308
+ col_filter: bool | None = None,
309
+ autofit_columns: bool | None = None,
310
+ ) -> None:
311
+ """Writing data to worksheet. Used for writing values to cells and formatting the cells
312
+ and
313
+
314
+ Args:
315
+ ws (Any): openpyxl worksheet
316
+ arr (np.ndarray): array containing the input data
317
+ fmt (Fmt): nested list containing dictionaries with the formatting info per cell
318
+ title (str | None, optional): Title to be inserted above the table. Defaults to None.
319
+ col_filter (bool | None, optional): Set column filter in excel. Defaults to None.
320
+ autofit_columns (bool | None, optional): Automatically fit column width. Defaults to None.
321
+ """
322
+ for row_idx, row in enumerate(arr):
323
+ for col_idx, _ in enumerate(row):
324
+ value = arr[row_idx][col_idx]
325
+ # Cell indices are not zero-indexed but one-indexed
326
+ cell = ws.cell(row_idx + 1, col_idx + 1, value)
327
+ # Get formatting for specific cell
328
+ cell_fmt = fmt[row_idx][col_idx]
329
+ for t, kwa in cell_fmt.items():
330
+ # The api for setting different kind of formatting options is not
331
+ # consistent therefore depeding on the formatting we want to set we have
332
+ # to use a different strategy
333
+ if t == "number_format":
334
+ # cell.number_format = "0.0"
335
+ setattr(cell, t, kwa["format"])
336
+ elif t.startswith("border"):
337
+ # cell.border = Border(bottom=Side(color="00a0e6"))
338
+ type_, side = t.split("_")
339
+ side_spec = Side(**kwa)
340
+ setattr(cell, type_, LOOKUP[type_](**{side: side_spec}))
341
+ else:
342
+ # cell.font = Font(color="B1D9F5", bold=True)
343
+ setattr(cell, t, LOOKUP[t](**kwa))
344
+
345
+ if col_filter:
346
+ filters = ws.auto_filter
347
+ filters.ref = f"A1:{excel_style(len(fmt), len(fmt[0]))}"
348
+
349
+ if autofit_columns:
350
+ _autofit_columns(ws)
351
+
352
+ if title:
353
+ _insert_title(ws, title)
354
+
355
+
356
+ # def _set_column_width(ws: Any, column_widths: list) -> None:
357
+ # for i, column_number in enumerate(range(ws.max_column)):
358
+ # column_letter = get_column_letter(column_letter)
359
+ # column_width = column_widths[i]
360
+ # ws.column_dimensions[column_letter].width = column_width
361
+
362
+
363
+ def _autofit_columns(ws: Any) -> None:
364
+ column_letters = tuple(
365
+ get_column_letter(col_number + 1) for col_number in range(ws.max_column)
366
+ )
367
+ for column_letter in column_letters:
368
+ ws.column_dimensions[column_letter].auto_fit = True
369
+
370
+
371
+ def _insert_title(ws: Any, title: str) -> None:
372
+ ws.insert_rows(0)
373
+ cell = ws.cell(1, 1, title)
374
+ for t, kwa in STYLES["title_bold"].items():
375
+ setattr(cell, t, LOOKUP[t](**kwa))
376
+
377
+
378
+ def write_table(
379
+ data: pd.DataFrame | dict[str, pd.DataFrame],
380
+ file: str,
381
+ header_row: int = 0,
382
+ title: str | dict[str, str] | None = None,
383
+ total_row: bool | None = None,
384
+ total_col: bool | None = None,
385
+ right_align_ids: list | None = None,
386
+ right_align_pattern: str | None = None,
387
+ right_align_numeric: bool | None = True,
388
+ left_align_ids: list | None = None,
389
+ left_align_pattern: str | None = None,
390
+ left_align_string: bool | None = True,
391
+ perc_ids: list | None = None,
392
+ perc_pattern: str | None = None,
393
+ perc_col_format: str | None = None,
394
+ blue_border: bool | None = True,
395
+ number_format: str = "0.0",
396
+ autofit_columns: bool | None = False,
397
+ col_filter: bool | None = False,
398
+ style: str = "old",
399
+ ):
400
+ """_summary_
401
+
402
+ Args:
403
+ data (pd.DataFrame | dict[pd.DataFrame]): dataframe or dicts with dataframes
404
+ name (str): name of excel file
405
+ header_row (int): Set the number of rows to be dark blue (zero-indexed). Defaults to 0 (top row)
406
+ title (str): Set the title above the table. In the case of multiple tables provide a dict in
407
+ which te keys correspond to the sheet name. Defaults to none
408
+ total_row (bool, optional): Color bottom row blue
409
+ total_col (bool, optional): Color last column blue.
410
+ right_align_ids (list, optional): The ids of the columns to right align. Defaults to None
411
+ right_align_pattern (str, optional): Pattern of columns to right align. Defaults to None.
412
+ right_align_numeric (bool, optional): Right align numeric columns. Defaults to True.
413
+ left_align_ids (list, optional): The ids of the columns to left align. Defaults to None.
414
+ left_align_pattern (str, optional): Pattern of columns to left align. Defaults to None.
415
+ left_align_string (bool, optional): Left align string columns. Defaults to True.
416
+ perc_ids (list, optional): The ids of the columns to format as percentage. Defaults to None.
417
+ perc_pattern (str, optional): The pattern of columns to format as percentage. Defaults to None.
418
+ perc_col_format (str, optional): The formatting string of percentage columns. Defaults to None.
419
+ col_filter (bool, optional): Set filter on columns. Defaults to False.
420
+ """
421
+
422
+ wb = Workbook()
423
+ # Empty sheet is created on Workbook creation
424
+ del wb["Sheet"]
425
+
426
+ set_global_style(style)
427
+
428
+ if not isinstance(data, dict):
429
+ data = {"Sheet1": data}
430
+
431
+ for sheet_name, df in data.items():
432
+ arr = df_to_array(df)
433
+
434
+ blue_rows = []
435
+ light_blue_rows = []
436
+ light_blue_cols = []
437
+ r_align_ids = []
438
+ l_align_ids = []
439
+ p_ids = []
440
+ title_tbl = None
441
+
442
+ if isinstance(header_row, int):
443
+ blue_rows.extend(list(range(0, header_row + 1)))
444
+
445
+ if title:
446
+ if isinstance(title, str):
447
+ title_tbl = title
448
+ elif isinstance(title, dict):
449
+ title_tbl = title.get(sheet_name)
450
+
451
+ if right_align_ids:
452
+ r_align_ids.extend(right_align_ids)
453
+
454
+ if right_align_pattern:
455
+ r_align_ids.extend(get_cols_id_with_pattern(df, right_align_pattern))
456
+
457
+ if right_align_numeric:
458
+ r_align_ids.extend(get_numeric_col_ids(df))
459
+
460
+ if left_align_ids:
461
+ r_align_ids.extend(left_align_ids)
462
+
463
+ if left_align_pattern:
464
+ l_align_ids.extend(get_cols_id_with_pattern(df, left_align_pattern))
465
+
466
+ if left_align_string:
467
+ l_align_ids.extend(get_string_cols_ids(df))
468
+
469
+ if perc_ids:
470
+ p_ids.extend(perc_ids)
471
+
472
+ if perc_pattern:
473
+ r_id = get_cols_id_with_pattern(df, perc_pattern)
474
+ p_ids.extend(r_id)
475
+ r_align_ids.extend(r_id)
476
+
477
+ if total_row:
478
+ light_blue_rows.append(arr.shape[0] - 1)
479
+
480
+ if total_col:
481
+ light_blue_cols.append(arr.shape[1] - 1)
482
+
483
+ ws = wb.create_sheet(sheet_name)
484
+
485
+ fmt = cell_formatting(
486
+ arr=arr,
487
+ default_format=STYLES["calibri"],
488
+ blue_row_ids=blue_rows,
489
+ light_blue_row_ids=light_blue_rows,
490
+ light_blue_col_ids=light_blue_cols,
491
+ left_align_ids=l_align_ids,
492
+ right_align_ids=r_align_ids,
493
+ perc_col_ids=p_ids,
494
+ perc_col_format=perc_col_format,
495
+ number_format=number_format,
496
+ blue_border=blue_border,
497
+ )
498
+
499
+ write_worksheet(
500
+ ws=ws,
501
+ arr=arr,
502
+ fmt=fmt,
503
+ title=title_tbl,
504
+ col_filter=col_filter,
505
+ autofit_columns=autofit_columns,
506
+ )
507
+
508
+ wb.save(file)
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import reduce
4
+ from typing import Iterable, Union
5
+
6
+ import polars as pl
7
+
8
+
9
+ def agg_multiple(
10
+ cols: Union[str, list[str]], funcs: Union[str, list[str]]
11
+ ) -> list[pl.Expr]:
12
+ if isinstance(cols, str):
13
+ cols = [cols]
14
+ if isinstance(funcs, str):
15
+ funcs = [funcs]
16
+
17
+ exprs = []
18
+ for col in cols:
19
+ for func in funcs:
20
+ exprs.append(getattr(pl.col(col), func)().alias(f"{col}_{func}"))
21
+
22
+ return exprs
23
+
24
+
25
+ def complete(df: pl.DataFrame, cols: Iterable[str]) -> pl.DataFrame:
26
+ combs = reduce(
27
+ lambda x, y: x.join(y, how="cross"),
28
+ [df.select(pl.col(col).unique()) for col in cols],
29
+ )
30
+
31
+ return combs.join(df, on=cols, how="left")