Source code for basic_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
from .heatmap import (
    CalendarDomain,
    CalendarLabel,
    CalendarSubDomain,
    CalendarDate,
    CalendarColor,
    CalendarMisc,
)

#----------------------------------------------------------------------------------------------------------------------#
# 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', text_color: Optional[str]=None, ): """ 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. text_color (Optional[str]): Override for the text color of the report. For a more granular approach use the config file """ 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, color_mode, text_color) self.color_mode = color_mode self.text_color = text_color 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.FullLoader) # 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 custom config file {cfg_file}!' logger.error(msg) raise RuntimeError(msg) with open(cfg_file, 'r') as f: custom_cfg = yaml.load(f, Loader=yaml.FullLoader) 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 def _make_cal_heatmap_css(self, output_dir: Path): css = [] for page in self.pages.values(): for hm in page.cal_heatmaps: css.append(hm.make_css()) css_file = output_dir / 'heatmap' / 'custom_cal_heatmap.css' css_file.write_text('\n'.join(css)) #-----# # 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, *, overwrite: bool=False): """ Save the report to file and add the CSS files Args: overwrite (bool): If true an existing report dir will be deleted first. Use with caution, there are no additional checks and balances implemented. If you rm your root its on you. """ # Use with caution. If you set it to the wrong dir, you have to deal with the consequences... if overwrite and self.report_dir.exists(): shutil.rmtree(self.report_dir) # 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 common css files and scripts from the package and the ones created by the color map src = SCRIPTS_DIR dst = self.report_dir / 'css_and_scripts' shutil.copytree(src, dst) self.color_map.make_css_files(dst) self._make_cal_heatmap_css(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, include_created_at: bool=True, color: Optional[str]=None): """ Add a report header to the current page Args: include_date (bool): Add the current date to the report include_created_at (bool): Include a subheader with the exact creation time of the report color (Optional[str]): The backgroound color for the header """ self.current.add_report_header(self.report_title, self.report_date, include_date, include_created_at, 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)
[docs] def add_section_title( self, title_text: str, text_color: Optional[str]=None, bar_color: Optional[str]=None, alignment: Optional[Literal['left', 'center', 'right']]=None, ): """ Add a section title to the page Args: title_text (str): Title text text_color (Optional[str]): Color of the title text bar_color (Optional[str]): Color of the bars alignment (Optional[str]): Alignment of the title. If left or right is chosen the bar will be omitted on the chosen side """ self.current.add_section_title(title_text, text_color, bar_color, alignment)
# 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_calendar_heatmap( self, *, misc: CalendarMisc | None = None, domain: CalendarDomain | None = None, label: CalendarLabel | None = None, subdomain: CalendarSubDomain | None = None, date: CalendarDate | None = None, color: CalendarColor | None = None, x: list[str] | None = None, y: list[float | int] | None = None, custom_config: dict[str, Any] | None = None, ): """ Add a calendar heatmap to the report Args: misc (CalendarMisc | None): The miscellaneous configuration for the calendar heatmap domain (CalendarDomain | None): The domain configuration for the calendar heatmap label (CalendarLabel | None): The label configuration for the calendar heatmap subdomain: (CalendarSubDomain | None): The subdomain configuration for the calendar heatmap date (CalendarDate | None): The date configuration for the calendar heatmap color (CalendarColor | None): The color configuration for the calendar heatmap x (list[str] | None): The time data array, e.g., dates y (list[float | int] | None): The corresponding value array custom_config (dict[str, Any] | None): A custom config dict to easily override any defaults set in the report. See the documentation for the calendar heatmap for an in depth discussion of the available parameters """ self.current.add_calendar_heatmap( misc=misc, domain=domain, label=label, subdomain=subdomain, date=date, color=color, x=x, y=y, custom_config=custom_config, )
[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_dictionary( self, dictionary: dict[str,str] | list[list[str]], key_color: str | None=None, key_width: str | None=None, value_color: str | None=None, ): """ Add a dictionary as key value list to the report Args: dictionary (dict[str, str] | list[list[str]]): The dictionary that should be added to the report. The key will be right aligned in one column, whereas the values will be left aligned in the next column key_color (str | None): The text color for the keys key_width (str | None): The width of the key column. Use any of the html specifications, e.g. 150px etc. value_color (str | None): The color of the value text """ self.current.add_dictionary(dictionary, key_color, key_width, value_color)
[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_cards(self, cards_type: Literal['group', 'deck', 'columns']): """ Open a navbar layout """ self.current.open_cards(cards_type)
[docs] def add_card( self, *, background_color: str | None = None, border_color: str | None = None, image: str | None = None, header_text: str | None = None, header_text_color: str | None = None, header_background_color: str | None = None, header_border_color: str | None = None, footer_text: str | None = None, footer_text_color: str | None = None, footer_background_color: str | None = None, footer_border_color: str | None = None, max_width: str='18rem', ): """ Add a section title to the page Args: background_color (str | None): The name of the background color for the whole card border_color (str | None): The name of the border color for the whole card image (str | None): A header image for the card. The source image will not be deleted! header_text (str | None): Text to be shown as header of the card header_text_color (str | None): The name of the header text color header_background_color (str | None): The name of the background color for the header section header_border_color (str | None): The name of the border color for the header section, only really footer_text (str | None): Text to be shown as footer of the card footer_text_color (str | None): The name of the footer text color footer_background_color (str | None): The name of the background color for the footer section footer_border_color (str | None): The name of the border color for the footer section, only really affects the line between the footer and the body max_width (str): The maximum width of the card """ self.current.add_card( background_color=background_color, border_color=border_color, image=image, header_text=header_text, header_text_color=header_text_color, header_background_color=header_background_color, header_border_color=header_border_color, footer_text=footer_text, footer_text_color=footer_text_color, footer_background_color=footer_background_color, footer_border_color=footer_border_color, max_width=max_width, )
[docs] def close_cards(self): """ Close the current navbar layout """ self.current.close_cards()
[docs] def open_navbar( self, loc: Optional[Literal['left', 'right', 'top']]=None, color: Optional[Literal['gray', 'red', 'blue', 'green']]=None, ): """ Open a navbar layout """ self.current.open_navbar(loc=loc, color=color)
[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()