#!/usr/bin/env python3 """ NAME: lf_report.py PURPOSE: This program is a helper class for reporting results for a lanforge python script. The class will generate an output directory based on date and time in the /home/lanforge/html-reports/ . If the reports-data is not present then the date and time directory will be created in the current directory. The banner and Candela Technology logo will be copied in the results directory. The results directory may be over written and many of the other paramaters during construction. Creating the date time directory on construction was a design choice. EXAMPLE: This is a helper class, a unit test is included at the bottom of the file. To test lf_report.py and lf_graph.py together use the lf_report_test.py file LICENSE: Free to distribute and modify. LANforge systems must be licensed. Copyright 2021 Candela Technologies Inc INCLUDE_IN_README """ # CAUTION: adding imports to this file which are not in update_dependencies.py is not advised import os import sys import shutil import datetime import pandas as pd import pdfkit import argparse import traceback import logging import importlib from matplotlib import pyplot as plt sys.path.append(os.path.join(os.path.abspath(__file__ + "../../../"))) logger = logging.getLogger(__name__) lf_logger_config = importlib.import_module("py-scripts.lf_logger_config") # internal candela references included during intial phases, to be deleted at future date # https://candelatech.atlassian.net/wiki/spaces/LANFORGE/pages/372703360/Scripting+Data+Collection+March+2021 # base report class class lf_report: def __init__(self, # _path the report directory under which the report directories will be created. _path="/home/lanforge/html-reports", _alt_path="", _date="", _title="LANForge Unit Test Run Heading", _table_title="LANForge Table Heading", _graph_title="LANForge Graph Title", _obj="", _obj_title="", _output_html="outfile.html", _output_pdf="outfile.pdf", _results_dir_name="LANforge_Test_Results_Unit_Test", _output_format='html', # pass in on the write functionality, current not used _dataframe="", _path_date_time="", _custom_css='custom-example.css'): # this is where the final report is placed. # other report paths, # _path is where the directory with the data time will be created if _path == "local" or _path == "here": self.path = os.path.abspath(__file__) logger.info("path set to file path: {}".format(self.path)) elif _alt_path != "": self.path = _alt_path logger.info("path set to alt path: {}".format(self.path)) else: self.path = _path logger.info("path set: {}".format(self.path)) self.dataframe = _dataframe self.text = "" self.title = _title self.table_title = _table_title self.graph_title = _graph_title self.date = _date self.output_html = _output_html if _output_html.lower().endswith(".pdf"): raise ValueError("HTML output file cannot end with suffix '.pdf'") self.path_date_time = _path_date_time self.report_location = "" # used by lf_check.py to know where to write the meta data "Report Location:::/home/lanforge/html-reports/wifi-capacity-2021-08-17-04-02-56" self.write_output_html = "" self.write_output_index_html = "" self.output_pdf = _output_pdf self.write_output_pdf = "" self.banner_html = "" self.footer_html = "" self.graph_titles = "" self.graph_image = "" self.csv_file_name = "" self.html = "" self.custom_html = "" self.pdf_link_html = "" self.objective = _obj self.obj_title = _obj_title self.description = "" self.desc_title = "" # self.systeminfopath = "" self.date_time_directory = "" self.log_directory = "" self.banner_directory = "artifacts" self.banner_file_name = "banner.png" # does this need to be configurable self.logo_directory = "artifacts" self.logo_file_name = "CandelaLogo2-90dpi-200x90-trans.png" # does this need to be configurable. self.logo_footer_file_name = "candela_swirl_small-72h.png" # does this need to be configurable. self.current_path = os.path.dirname(os.path.abspath(__file__)) self.custom_css = _custom_css # note: the following 3 calls must be in order self.set_date_time_directory(_date, _results_dir_name) self.build_date_time_directory() self.build_log_directory() self.font_file = "CenturyGothic.woff" # move the banners and candela images to report path self.copy_banner() self.copy_css() self.copy_logo() self.copy_logo_footer() def copy_banner(self): banner_src_file = str(self.current_path) + '/' + str(self.banner_directory) + '/' + str(self.banner_file_name) banner_dst_file = str(self.path_date_time) + '/' + str(self.banner_file_name) # print("banner src_file: {}".format(banner_src_file)) # print("dst_file: {}".format(banner_dst_file)) shutil.copy(banner_src_file, banner_dst_file) def move_data(self, directory=None, _file_name=None, directory_name=None): if directory_name is None: _src_file = str(self.current_path) + '/' + str(_file_name) if directory is None: _dst_file = str(self.path_date_time) else: _dst_file = str(self.path_date_time) + '/' + str(directory) + '/' + str(_file_name) else: _src_file = str(self.current_path) + '/' + str(directory_name) _dst_file = str(self.path_date_time) + '/' + str(directory_name) shutil.move(_src_file, _dst_file) def copy_css(self): reportcss_src_file = str(self.current_path) + '/' + str(self.banner_directory) + '/report.css' # print("copy_css: source file is: "+reportcss_src_file) reportcss_dest_file = str(self.path_date_time) + '/report.css' customcss_src_file = str(self.current_path) + '/' + str(self.banner_directory) + '/' + str(self.custom_css) customcss_dest_file = str(self.path_date_time) + '/custom.css' font_src_file = str(self.current_path) + '/' + str(self.banner_directory) + '/' + str(self.font_file) font_dest_file = str(self.path_date_time) + '/' + str(self.font_file) shutil.copy(reportcss_src_file, reportcss_dest_file) shutil.copy(customcss_src_file, customcss_dest_file) shutil.copy(font_src_file, font_dest_file) def copy_logo(self): logo_src_file = str(self.current_path) + '/' + str(self.logo_directory) + '/' + str(self.logo_file_name) logo_dst_file = str(self.path_date_time) + '/' + str(self.logo_file_name) # print("logo_src_file: {}".format(logo_src_file)) # print("logo_dst_file: {}".format(logo_dst_file)) shutil.copy(logo_src_file, logo_dst_file) def copy_logo_footer(self): logo_footer_src_file = str(self.current_path) + '/' + str(self.logo_directory) + '/' + str( self.logo_footer_file_name) logo_footer_dst_file = str(self.path_date_time) + '/' + str(self.logo_footer_file_name) # print("logo_footer_src_file: {}".format(logo_footer_src_file)) # print("logo_footer_dst_file: {}".format(logo_footer_dst_file)) shutil.copy(logo_footer_src_file, logo_footer_dst_file) def move_graph_image(self, ): graph_src_file = str(self.graph_image) graph_dst_file = str(self.path_date_time) + '/' + str(self.graph_image) logger.info("graph_src_file: {}".format(graph_src_file)) logger.info("graph_dst_file: {}".format(graph_dst_file)) shutil.move(graph_src_file, graph_dst_file) def move_csv_file(self): csv_src_file = str(self.csv_file_name) csv_dst_file = str(self.path_date_time) + '/' + str(self.csv_file_name) logger.info("csv_src_file: {}".format(csv_src_file)) logger.info("csv_dst_file: {}".format(csv_dst_file)) shutil.move(csv_src_file, csv_dst_file) def set_path(self, _path): self.path = _path def set_date_time_directory(self, _date, _results_dir_name): self.date = _date self.results_dir_name = _results_dir_name if self.date != "": self.date_time_directory = str(self.date) + str("_") + str(self.results_dir_name) else: self.date = str(datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")).replace(':', '-') self.date_time_directory = self.date + str("_") + str(self.results_dir_name) def build_date_time_directory(self): if self.date_time_directory == "": self.set_date_time_directory() self.path_date_time = os.path.join(self.path, self.date_time_directory) logger.info("path_date_time {}".format(self.path_date_time)) try: if not os.path.exists(self.path_date_time): os.mkdir(self.path_date_time) except Exception as x: traceback.print_exception(Exception, x, x.__traceback__, chain=True) self.path_date_time = os.path.join(self.current_path, self.date_time_directory) if not os.path.exists(self.path_date_time): os.mkdir(self.path_date_time) logger.info("report path : {}".format(self.path_date_time)) def build_log_directory(self): if self.log_directory == "": self.log_directory = os.path.join(self.path_date_time, "log") try: if not os.path.exists(self.log_directory): os.mkdir(self.log_directory) except Exception as x: traceback.print_exception(Exception, x, x.__traceback__, chain=True) logger.critical("exception making {}".format(self.log_directory)) exit(1) def build_x_directory(self, directory_name=None): directory = None if directory_name: directory = os.path.join(self.path_date_time, str(directory_name)) try: if not os.path.exists(directory): os.mkdir(directory) except Exception as x: traceback.print_exception(Exception, x, x.__traceback__, chain=True) logger.critical("exception making {}".format(directory)) exit(1) def set_text(self, _text): self.text = _text def set_title(self, _title): self.title = _title def set_table_title(self, _table_title): self.table_title = _table_title def set_graph_title(self, _graph_title): self.graph_title = _graph_title # sets the csv file name as graph title def set_csv_filename(self, _graph_title): fname, ext = os.path.splitext(_graph_title) self.csv_file_name = fname + ".csv" def write_dataframe_to_csv(self, _index=False): csv_file = "{path_date_time}/{csv_file_name}".format(path_date_time=self.path_date_time,csv_file_name=self.csv_file_name) self.dataframe.to_csv(csv_file,index=_index) # The _date is set when class is enstanciated / created so this set_date should be used with caution, used to synchronize results def set_date(self, _date): self.date = _date def set_table_dataframe(self, _dataframe): self.dataframe = _dataframe def set_table_dataframe_from_csv(self, _csv): self.dataframe = pd.read_csv(_csv) def set_table_dataframe_from_csv_sep_tab(self, _csv): self.dataframe = pd.read_csv(_csv, sep='\t') # TODO def set_table_dataframe_from_xlsx(self,_xlsx): self.dataframe = pd.read_excel(_xlsx) def set_custom_html(self, _custom_html): self.custom_html = _custom_html def set_obj_html(self, _obj_title, _obj): self.objective = _obj self.obj_title = _obj_title def set_desc_html(self, _desc_title, _desc): self.description = _desc self.desc_title = _desc_title def set_graph_image(self, _graph_image): self.graph_image = _graph_image def get_date(self): return self.date def get_path(self): return self.path def get_parent_path(self): parent_path = os.path.dirname(self.path) return parent_path # get_path_date_time, get_report_path and need to be the same def get_path_date_time(self): return self.path_date_time def get_report_path(self): return self.path_date_time def get_flat_dir_report_path(self): return self.path def get_log_path(self): return self.log_directory def file_add_path(self, file): output_file = str(self.path_date_time) + '/' + str(file) logger.info("output file {}".format(output_file)) return output_file # Report Location:::/ as a key in lf_check.py def write_report_location(self): self.report_location = self.path_date_time logger.info("Report Location:::{report_location}".format(report_location=self.report_location)) def write_html(self): if not self.output_html: logger.info("no html file name, skipping report generation") return if self.output_html.lower().endswith(".pdf"): raise ValueError("write_html: HTML filename [%s] should not end with .pdf" % self.output_html) if self.write_output_html.endswith(".pdf"): raise ValueError("wrong suffix for an HTML file: %s" % self.write_output_html) self.write_output_html = str(self.path_date_time) + '/' + str(self.output_html) logger.info("write_output_html: {}".format(self.write_output_html)) try: test_file = open(self.write_output_html, "w") test_file.write(self.html) test_file.close() except Exception as x: traceback.print_exception(Exception, x, x.__traceback__, chain=True) logger.info("write_html failed") return self.write_output_html def write_index_html(self): # LAN-1535 scripting: test_l3.py output masks other output when browsing. # consider renaming index.html to readme.html # self.write_output_index_html = str(self.path_date_time) + '/' + str("index.html") if not self.output_html: logger.info("no html file name, skipping report generation") return self.write_output_index_html = str(self.path_date_time) + '/' + str("readme.html") logger.info("write_output_index_html: {}".format(self.write_output_index_html)) try: test_file = open(self.write_output_index_html, "w") test_file.write(self.html) test_file.close() except Exception as x: traceback.print_exception(Exception, x, x.__traceback__, chain=True) logger.info("write_index_html failed") return self.write_output_index_html def write_html_with_timestamp(self): if not self.output_html: logger.info("no html file name, skipping report generation") return if self.output_html.lower().endswith(".pdf"): raise ValueError("write_html_with_timestamp: will not save file with PDF suffix [%s]" % self.output_html) self.write_output_html = "{}/{}-{}".format(self.path_date_time, self.date, self.output_html) logger.info("write_output_html: {}".format(self.write_output_html)) try: test_file = open(self.write_output_html, "w") test_file.write(self.html) test_file.close() except Exception as x: traceback.print_exception(Exception, x, x.__traceback__, chain=True) logger.warning("write_html failed") return self.write_output_html # https://wkhtmltopdf.org/usage/wkhtmltopdf.txt # page_size A4, A3, Letter, Legal # orientation Portrait , Landscape def write_pdf(self, _page_size='A4', _orientation='Portrait'): # write logic to generate pdf here # wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb # sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb if not self.output_pdf: logger.info("write_pdf: no pdf file name, skipping pdf output") return options = {"enable-local-file-access": None, 'orientation': _orientation, 'page-size': _page_size} # prevent error Blocked access to file self.write_output_pdf = str(self.path_date_time) + '/' + str(self.output_pdf) pdfkit.from_file(self.write_output_html, self.write_output_pdf, options=options) # https://wkhtmltopdf.org/usage/wkhtmltopdf.txt # page_size A4, A3, Letter, Legal # orientation Portrait , Landscape def write_pdf_with_timestamp(self, _page_size='A4', _orientation='Portrait'): # write logic to generate pdf here # wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb # sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb if not self.output_pdf: logger.info("write_pdf_with_timestamp: no pdf file name, skipping pdf output") return options = {"enable-local-file-access": None, 'orientation': _orientation, 'page-size': _page_size} # prevent error Blocked access to file self.write_output_pdf = "{}/{}-{}".format(self.path_date_time, self.date, self.output_pdf) pdfkit.from_file(self.write_output_html, self.write_output_pdf, options=options) def get_pdf_path(self): pdf_link_path = "{}/{}-{}".format(self.path_date_time, self.date, self.output_pdf) return pdf_link_path # used for relative pathing def get_pdf_file(self): if not self.output_pdf: logger.info("get_pdf_file: no pdf name, returning None") return None pdf_file = "{}-{}".format(self.date, self.output_pdf) return pdf_file def build_pdf_link(self, _pdf_link_name, _pdf_link_path): self.pdf_link_html = """ {pdf_link_name}
""".format(pdf_link_path=_pdf_link_path, pdf_link_name=_pdf_link_name) self.html += self.pdf_link_html def build_link(self, _link_name, _link_path): self.link = """ {link_name}
""".format(link_path=_link_path, link_name=_link_name) self.html += self.link def generate_report(self): self.write_html() if self.output_pdf: self.write_pdf() def build_all(self): self.build_banner() self.start_content_div() self.build_table_title() self.build_table() self.end_content_div() def get_html_head(self, title='Untitled'): return """ {title} """.format(title=title) def build_banner(self): # NOTE: {{ }} are the ESCAPED curly braces # JBR removed deep indentation of html tag because it makes browser view-source is hard to debug # JBR suggests rename method to start_html_doc() self.banner_html = """ {head_tag}
""".format( head_tag=self.get_html_head(title=self.title), title=self.title, date=self.date, ) self.html += self.banner_html def build_banner_left(self): # NOTE: {{ }} are the ESCAPED curly braces # JBR suggests rename method to start_html_doc() # This method violates DRY, if the ID of the body/div#BannerBack/div element is actually necessary # to specify, this needs to be made a parameter for build_banner() or start_html_doc() self.banner_html = """ {head_tag}

{title}

{date}

""".format( head_tag=self.get_html_head(title=self.title), title=self.title, date=self.date, ) self.html += self.banner_html def build_banner_left_h2_font(self): # NOTE: {{ }} are the ESCAPED curly braces # JBR suggests rename method to start_html_doc() # This method violates DRY, if the ID of the body/div#BannerBack/div element is actually necessary # to specify, this needs to be made a parameter for build_banner() or start_html_doc() self.banner_html = """ {head_tag}

{title}

{date}

""".format( head_tag=self.get_html_head(title=self.title), title=self.title, date=self.date, ) self.html += self.banner_html def build_table_title(self): self.table_title_html = """

{title}

""".format(title=self.table_title) self.html += self.table_title_html def start_content_div2(self): self.html += "\n
\n" def start_content_div(self): self.html += "\n
\n" def build_text(self): # please do not use 'style=' tags unless you cannot override a class self.text_html = """

{text}

\n
""".format(text=self.text) self.html += self.text_html def build_text_simple(self): # please do not use 'style=' tags unless you cannot override a class self.text_html = """

{text}

""".format(text=self.text) self.html += self.text_html def build_date_time(self): self.date_time = str(datetime.datetime.now().strftime("%Y-%m-%d-%H-h-%m-m-%S-s")).replace(':', '-') return self.date_time def build_path_date_time(self): try: self.path_date_time = os.path.join(self.path, self.date_time) os.mkdir(self.path_date_time) except Exception as x: traceback.print_exception(Exception, x, x.__traceback__, chain=True) curr_dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) self.path_date_time = os.path.join(curr_dir_path, self.date_time) os.mkdir(self.path_date_time) def pass_fail_background(self, cell_value): highlight_success = 'background-color: #4af84a;' highlight_fail = 'background-color: #ff1300;' if type(cell_value) in [str]: if cell_value == "Success": return highlight_success elif cell_value == "Failed": return highlight_fail def build_table(self): self.dataframe_html = self.dataframe.to_html(index=False, justify='center') # have the index be able to be passed in. self.html += self.dataframe_html def pass_failed_build_table(self): self.dataframe_html = self.dataframe.style.hide_index(subset=None, level=None, names=False).applymap \ (self.pass_fail_background).to_html(index=False, justify='center') # have the index be able to be passed in. self.html += self.dataframe_html def save_csv(self, file_name, save_to_csv_data): save_to_csv_data.to_csv(str(self.path_date_time) + "/" + file_name) def save_pie_chart(self, pie_chart_data): explode = (0, 0.1) pie_chart = pie_chart_data.plot.pie(y='Pass/Fail', autopct="%.2f%%", explode=explode, figsize=(10, 10), shadow=True, startangle=90, colors=['#4af84a', '#ff1300']) plt.savefig(str(self.path_date_time) + '/pie-chart.png') def save_bar_chart(self, xlabel, ylabel, bar_chart_data, name): plot = bar_chart_data.plot.bar(alpha=0.9, rot=0, width=0.9, linewidth=0.9, figsize=(10, 6)) plot.legend(bbox_to_anchor=(1.0, 1.0)) # plot.spines['right'].set_visible(False) # plot.spines['top'].set_visible(False) # plot.set_title(name) for p in plot.patches: height = p.get_height() plot.annotate('{}'.format(height), xy=(p.get_x() + p.get_width() / 2, height), xytext=(0, 0), # 3 points vertical offset textcoords="offset points", rotation=90, annotation_clip=False, ha='center', va='bottom') # plt.xlabel(xlabel) plt.xticks(rotation=0, horizontalalignment='right', fontweight='light', fontsize='small', ) plt.ylabel(ylabel) plt.tight_layout() plt.savefig(str(self.path_date_time) + '/' + name + '.png') def test_setup_table(self, test_setup_data, value): if test_setup_data is None: return None else: var = "" for i in test_setup_data: var = var + "" + i + "" + str(test_setup_data[i]) + "" setup_information = """
""" + str(value) + """ """ + var + """

""" self.html += setup_information def build_footer(self): self.footer_html = """ """ self.html += self.footer_html def build_footer_no_png(self): self.footer_html = """ """ self.html += self.footer_html def copy_js(self): self.html += """ """ def build_custom(self): self.html += self.custom_html def build_objective(self): self.obj_html = """

{title}

{objective}

""".format(title=self.obj_title, objective=self.objective) self.html += self.obj_html def build_description(self): self.obj_html = """

{title}

{description}

""".format(title=self.desc_title, description=self.description) self.html += self.obj_html def build_graph_title(self): self.table_graph_html = """

{title}

""".format(title=self.graph_title) self.html += self.table_graph_html def build_graph(self): self.graph_html_obj = """ """.format(image=self.graph_image) self.html += self.graph_html_obj def build_graph_without_border(self): self.graph_html_obj = """ """.format(image=self.graph_image) self.html += self.graph_html_obj def end_content_div(self): self.html += "\n
\n" def build_chart_title(self, chart_title): self.chart_title_html = """

{title}

""".format(title=chart_title) self.html += self.chart_title_html def build_chart(self, name): self.chart_html_obj = """ """.format(image=name) self.html += self.chart_html_obj def build_chart_custom(self, name, align='center',padding='15px',margin='5px 5px 2em 5px',width='500px',height='500px'): self.chart_html_obj = """ """.format(image=name,align=align,padding=padding,margin=margin,width=width, height=height) self.html += self.chart_html_obj def build_banner_cover(self): # NOTE: {{ }} are the ESCAPED curly braces # JBR suggests rename method to start_html_doc() # This method violates DRY, if the ID of the body/div#BannerBack/div element is actually necessary # to specify, this needs to be made a parameter for build_banner() or start_html_doc() self.banner_html = """ {head_tag}

{title}

{date}

""".format( head_tag=self.get_html_head(title=self.title), title=self.title, date=self.date, ) self.html += self.banner_html # Unit Test if __name__ == "__main__": help_summary = '''\ This script is designed to generate reports in file formats such as PDF and HTML, accommodating various user preferences. The reports can encompass a range of elements, including graphs, tables, and customizable objectives, tailored to meet specific user requirements ''' parser = argparse.ArgumentParser( prog="lf_report.py", formatter_class=argparse.RawTextHelpFormatter, description="Reporting library Unit Test") parser.add_argument('--lfmgr', help='sample argument: where LANforge GUI is running', default='localhost') # the args parser is not really used , this is so the report is not generated when testing # the imports with --help parser.add_argument('--help_summary', help='Show summary of what this script does', default=None, action="store_true") args = parser.parse_args() # help summary if args.help_summary: print(help_summary) exit(0) logger.info("LANforge manager {lfmgr}".format(lfmgr=args.lfmgr)) # Testing: generate data frame dataframe = pd.DataFrame({ 'product': ['CT521a-264-1ac-1n', 'CT521a-1ac-1ax', 'CT522-264-1ac2-1n', 'CT523c-2ac2-db-10g-cu', 'CT523c-3ac2-db-10g-cu', 'CT523c-8ax-ac10g-cu', 'CT523c-192-2ac2-1ac-10g'], 'radios': [1, 1, 2, 2, 6, 9, 3], 'MIMO': ['N', 'N', 'N', 'Y', 'Y', 'Y', 'Y'], 'stations': [200, 64, 200, 128, 384, 72, 192], 'mbps': [300, 300, 300, 10000, 10000, 10000, 10000] }) logger.info(dataframe) # Testing: generate data frame dataframe2 = pd.DataFrame({ 'station': [1, 2, 3, 4, 5, 6, 7], 'time_seconds': [23, 78, 22, 19, 45, 22, 25] }) report = lf_report() report.set_title("Banner Title One") report.build_banner() report.set_table_title("Title One") report.build_table_title() report.set_table_dataframe(dataframe) report.build_table() report.set_table_title("Title Two") report.build_table_title() report.set_table_dataframe(dataframe2) report.build_table() report.build_chart_title('default width') report.build_chart("banner.png") report.build_chart_title('custom width') report.build_chart_custom(name="banner.png",width="1000") # report.build_all() # report.build_footer() report.build_footer_no_png() html_file = report.write_html() logger.info("returned file ") logger.info(html_file) report.write_pdf() logger.info("report path {}".format(report.get_path()))