Source code for simple_report.report

import datetime
import shutil
import yaml

from loguru import logger
from pandas import DataFrame
from pathlib import Path
from typing import Any, Union, Optional, Literal, Callable

from . import CONFIG_DIR, SCRIPTS_DIR
from .page import ReportPage
from .utils import default, verify_color_mode, ColorMap

#----------------------------------------------------------------------------------------------------------------------#
# region REPORT
#----------------------------------------------------------------------------------------------------------------------#
[docs] class Report: """ Main report object """ def __init__( self, report_dir: Union[str, Path], report_title: Optional[str]=None, report_date: Optional[Union[str, int, datetime.date, datetime.datetime]]=None, config_file: Optional[Union[str, Path]]=None, color_mode: str='light', ): """ Initialize a new report Args: report_dir (Union[str, Path]): The main output dir which will contain the report report_title (Optional[str]): The report title, if None is given it will be called `Unnamed Report` report_date (Optional[Union[str, int, datetime.date, datetime.datetime]]): The report date, if none is given today will be used config_file (Optional[Union[str, Path]]): Path to the user config file that will be used. If none is given the default config file will be read. color_mode (str): Either `light` or `dark. """ self.report_date = default(report_date, datetime.date.today()) self.report_title = default(report_title, 'Unnamed Report') self.report_dir = Path(report_dir) self.config = self._load_config(config_file) self.color_map = ColorMap(self.config) self.color_mode = color_mode self.pages: dict[str, ReportPage] = {} self.add_page('main', subpage=False, color_mode=color_mode) self.set_current_page('main') self.global_links = [] #---------------# # Magic Methods # #---------------# def __getitem__(self, key: str) -> ReportPage: """ Simple access to the subpages Args: key (str): The page name to access Returns: ReportPage - The page object for the given page name """ return self.pages[key] #---------# # Private # #---------# def _load_config(self, cfg_file: Optional[Union[str, Path]]=None) -> dict[str, Any]: """ Load a report config file Args: cfg_file (Optional[Union[str, Path]]): The config file to load. If none is given the default config file will be read Returns: dict[str, Any] - The config file content as dict """ # Read default config default_cfg_file = CONFIG_DIR / 'default.yaml' if not default_cfg_file.exists(): msg = f'Could not find default config file {default_cfg_file}!' logger.error(msg) raise RuntimeError(msg) with open(default_cfg_file, 'r') as f: default_cfg = yaml.load(f, Loader=yaml.BaseLoader) # Read custom config if given if cfg_file is not None: cfg_file = Path(cfg_file) if not cfg_file.exists(): msg = f'Could not find default config file {default_cfg_file}!' logger.error(msg) raise RuntimeError(msg) with open(cfg_file, 'r') as f: custom_cfg = yaml.load(f, Loader=yaml.BaseLoader) else: custom_cfg = {} cfg = default_cfg for k,v in custom_cfg.items(): if k != 'custom_colors': cfg[k] = v else: for ck,cv in v.items(): if ck not in cfg['custom_colors']: cfg['custom_colors'][ck] = cv else: msg = (f'Tried to set color with name {ck}, which is already defined by the report class itself' f'Please rename the color and try again.') logger.error(msg) raise RuntimeError(msg) return cfg #-----# # API # #-----# # Report structure methods # ------------------------
[docs] def add_page(self, name: str, subpage: bool=True, color_mode: Optional[str]=None): """ Add a new page to the report Args: name (str): The name of the report page subpage (bool): Indicates this is a subpage of the report color_mode (Optional[str]): Color mode of the page, either `light` or `dark` """ color_mode = verify_color_mode(default(color_mode, self.color_mode)) self.pages[name] = ReportPage(self.report_dir, name, self.config, self.color_map, subpage, color_mode)
[docs] def dump(self): """ Save the report to file and add the CSS files """ # Dump all of the report pages for page in self.pages.values(): if self.global_links: page._make_global_link_navbar(self.global_links) #noqa: SLF001 page._dump() #noqa: SLF001 # Add the css and script dir src = SCRIPTS_DIR dst = self.report_dir / 'css_and_scripts' shutil.copytree(src, dst)
[docs] def set_current_page(self, page_name: str): """ Set the current page that is being worked on Args: page_name (str): The page you want to switch to """ if page_name not in self.pages: msg = f'Unkown page {page_name}. Known: {self.pages.keys()}' logger.error(msg) raise RuntimeError(msg) self.current: ReportPage = self.pages[page_name]
# Headers & special sections # --------------------------
[docs] def add_report_header(self, include_date: bool=True, color: str='primary'): """ Add a report header to the current page Args: include_date (bool): Add the current date to the report color (str): The backgroound color for the header """ self.current.add_report_header(self.report_title, self.report_date, include_date, color)
[docs] def add_error_warning_info_section(self, **kwargs): """ Add a section detailing errors, warnings, and info """ self.current.add_error_warning_info_section(**kwargs)
[docs] def add_header(self, header_text: str, color: Optional[str]=None): """ Add a header to the current page Args: header_text (str): Header text. You can use any valid HTML code inside the text, e.g., links and such. color (Optional[str]): Color of the header box """ self.current.add_header(header_text, color)
[docs] def add_sub_header(self, header_text: str, color: Optional[str]=None, sub_level: Optional[int]=None): """ Add a sub header to the current page Args: header_text (str): Header text. You can use any valid HTML code inside the text, e.g., links and such. color (Optional[str]): Color of the header box sub_level (Optional[int]): Shrinks the width of the header box. Levels can be 1 to 5. The higher the level the smaller the box """ self.current.add_sub_header(header_text, color, sub_level)
# Miscellaneous elements # ----------------------
[docs] def add_text(self, text: str, color: Optional[str]=None, align: Optional[Literal['left', 'center', 'right']]=None): """ Add (colored) text to the page This will create a new paragraph. So if you want to mix colors within one paragraph you'll have to do this yourself for now Args: text (str): Your text color (Optional[str]): One of the supported colors align (Optional[Literal['left', 'center', 'right']]): Alignment of text """ self.current.add_text(text, color, align)
[docs] def add_table( self, table_data: Union[DataFrame, list[list[Any]]], formatters: Optional[Union[str, Callable, list[str], list[Callable]]]=None, options: Optional[list[str]]=None, size: Optional[int]=None, align: Optional[Literal['left','center','right']]=None, caption: Optional[str]=None, footers: Optional[list[list[Any]]]=None, order: Optional[list[list[Any]]]=None, color: Optional[str]=None, ): """ Add a table to the page. ``table_data`` can either be a pandas ``DataFrame`` or a list of lists. For the former we assume indices and column labels are already properly formatted. For the latter we assume the first item contains the headers. Args: table_data (Union[DataFrame, list[list[Any]]]): Data to tabelize. formatters (Optional[Union[str, Callable, list[str], list[Callable]]]): Formatters applied to each table element. If a list is provided it must match the number of columns. A single formatter is applied to all columns. The default formatter is ``str()``. options (Optional[list[str]]): Turn optional elements of the DataTable on. Supported options: - ``page`` - Split long tables into pages of 10 (configurable) - ``info`` - Show number of rows - ``search`` - Add a search field - ``no_sort`` - Disable initial sorting - ``color_negative_values`` - Highlight values < 0 in red - ``color_positive_values`` - Highlight values > 0 in green - ``full_width`` - Table spans full window width size (Optional[int]): Sets the width of the table (1-12). ``12`` uses full container width. align (Optional[str]): Alignment of cell content. caption (Optional[str]): Text shown below the table. footers (Optional[list[list[Any]]]): Rows always shown at the bottom of the table. order (Optional[list[list[Any]]]): Default table order in the form ``[[col_idx, 'asc' | 'desc'], ...]``. color (Optional[str]): Text color. Automatically determined if omitted. """ self.current.add_table(table_data, formatters, options, size, align, caption, footers, order, color)
[docs] def add_list(self, list_items: list[str]): """ Add a list to the current page Args: list_items (list[str]): A list to be added to the page """ self.current.add_list(list_items)
[docs] def add_image(self, image_source: Union[str, Path], remove_source: Optional[bool]=None, responsive: Optional[bool]=None, center: Optional[bool]=None, ): """ Add an image to the current page Args: image_source (Union[str, Path]): The path to the image to add remove_source (Optional[bool]): If true the source will be deleted and only acopy in the report dir is kept responsive (Optional[bool]): If true the image will automatically be resized to match the outer container center (Optional[bool]): If true the image will be centered """ self.current.add_image(image_source, remove_source, responsive, center)
[docs] def add_custom_html_code(self, html: str): """ Add custom / generic html code Args: html (str): The HTML string. """ self.current.add_custom_html_code(html)
# Layouting # ---------
[docs] def open_columns(self): """ Start a column layout """ self.current.open_columns()
[docs] def add_column(self): """ Add another column to the current column layout """ self.current.add_column()
[docs] def close_columns(self): """ Close the current column layout """ self.current.close_columns()
[docs] def open_tabs(self): """ Open a tab layout""" self.current.open_tabs()
[docs] def add_tab(self, name: str): """ Add another tab to the current tab layout Args: name (str): The text shown on the tab """ self.current.add_tab(name)
[docs] def close_tabs(self): """ Close the current tab layout""" self.current.close_tabs()
[docs] def open_navbar(self, **kwargs): """ Open a navbar layout """ self.current.open_navbar(**kwargs)
[docs] def add_navbar_item(self, name: str): """ Add another navbar item to the current navbar layout Args: name (str): The text shown on the navbar pill """ self.current.add_navbar_item(name)
[docs] def close_navbar(self): """ Close the current navbar layout """ self.current.close_navbar()
[docs] def open_accordion(self, **kwargs): """ Open a accordion layout""" self.current.open_accordion(**kwargs)
[docs] def add_accordion_item(self, name: str): """ Add another accordion item to the current accordion layout Args: name (str): The text shown on the accordion """ self.current.add_accordion_item(name)
[docs] def close_accordion(self): """ Close the current accorion layout """ self.current.close_accordion()
[docs] def open_sublevel(self, **kwargs): """ Open a sub-level layout """ self.current.open_sublevel(**kwargs)
[docs] def close_sublevel(self): """ Close the current sub-level layout """ self.current.close_sublevel()