From 9d6396c31bf85288bdccd3ab761336dcadcc42b6 Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Wed, 23 Apr 2025 12:26:09 +0200 Subject: [PATCH 01/15] =?UTF-8?q?=F0=9F=9A=A7=E2=99=BB=EF=B8=8F=20Refactor?= =?UTF-8?q?=20creating=20and=20writing=20components=20streamlit=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - writing of imports + component code (write python file) - component - fct map for generation - building list of components --- src/vuegen/streamlit_reportview.py | 172 +++++++++++++++-------------- 1 file changed, 89 insertions(+), 83 deletions(-) diff --git a/src/vuegen/streamlit_reportview.py b/src/vuegen/streamlit_reportview.py index 534a537..3fd441a 100644 --- a/src/vuegen/streamlit_reportview.py +++ b/src/vuegen/streamlit_reportview.py @@ -13,6 +13,15 @@ from .utils.variables import make_valid_identifier +def write_python_file(fpath: str, imports: list[str], contents: list[str]) -> None: + with open(fpath, "w", encoding="utf8") as f: + # Write imports at the top of the file + f.write("\n".join(imports) + "\n\n") + + # Write the subsection content (descriptions, plots) + f.write("\n".join(contents)) + + class StreamlitReportView(r.WebAppReportView): """ A Streamlit-based implementation of the WebAppReportView abstract base class. @@ -38,6 +47,15 @@ def __init__( else: self.report.logger.info("running in a normal Python process") + self.components_fct_map = { + r.ComponentType.PLOT: self._generate_plot_content, + r.ComponentType.DATAFRAME: self._generate_dataframe_content, + r.ComponentType.MARKDOWN: self._generate_markdown_content, + r.ComponentType.HTML: self._generate_html_content, + r.ComponentType.APICALL: self._generate_apicall_content, + r.ComponentType.CHATBOT: self._generate_chatbot_content, + } + def generate_report( self, output_dir: str = SECTIONS_DIR, static_dir: str = STATIC_FILES_DIR ) -> None: @@ -338,65 +356,79 @@ def _generate_sections(self, output_dir: str, static_dir: str) -> None: f"Processing section '{section.id}': '{section.title}' - {len(section.subsections)} subsection(s)" ) - if section.subsections: - # Iterate through subsections and integrate them into the section file - for subsection in section.subsections: - self.report.logger.debug( - f"Processing subsection '{subsection.id}': '{subsection.title} - {len(subsection.components)} component(s)'" - ) - try: - # Create subsection file - _subsection_name = make_valid_identifier(subsection.title) - subsection_file_path = ( - Path(output_dir) - / section_name_var - / f"{_subsection_name}.py" - ) - - # Generate content and imports for the subsection - subsection_content, subsection_imports = ( - self._generate_subsection( - subsection, static_dir=static_dir - ) - ) - - # Flatten the subsection_imports into a single list - flattened_subsection_imports = [ - imp for sublist in subsection_imports for imp in sublist - ] - - # Remove duplicated imports - unique_imports = list(set(flattened_subsection_imports)) - - # Write everything to the subsection file - with open(subsection_file_path, "w") as subsection_file: - # Write imports at the top of the file - subsection_file.write( - "\n".join(unique_imports) + "\n\n" - ) - - # Write the subsection content (descriptions, plots) - subsection_file.write("\n".join(subsection_content)) - - self.report.logger.info( - f"Subsection file created: '{subsection_file_path}'" - ) - except Exception as subsection_error: - self.report.logger.error( - f"Error processing subsection '{subsection.id}' '{subsection.title}' in section '{section.id}' '{section.title}': {str(subsection_error)}" - ) - raise - else: + if not section.subsections: self.report.logger.warning( - f"No subsections found in section: '{section.title}'. To show content in the report, add subsections to the section." + f"No subsections found in section: '{section.title}'. " + "To show content in the report, add subsections to the section." + ) + continue + + # Iterate through subsections and integrate them into the section file + for subsection in section.subsections: + self.report.logger.debug( + f"Processing subsection '{subsection.id}': '{subsection.title} - {len(subsection.components)} component(s)'" ) + try: + # Create subsection file + _subsection_name = make_valid_identifier(subsection.title) + subsection_file_path = ( + Path(output_dir) + / section_name_var + / f"{_subsection_name}.py" + ) + + # Generate content and imports for the subsection + subsection_content, subsection_imports = ( + self._generate_subsection(subsection) + ) + + write_python_file( + fpath=subsection_file_path, + imports=subsection_imports, + contents=subsection_content, + ) + self.report.logger.info( + f"Subsection file created: '{subsection_file_path}'" + ) + except Exception as subsection_error: + self.report.logger.error( + f"Error processing subsection '{subsection.id}' '{subsection.title}' " + f"in section '{section.id}' '{section.title}': {str(subsection_error)}" + ) + raise + except Exception as e: self.report.logger.error(f"Error generating sections: {str(e)}") raise - def _generate_subsection( - self, subsection, static_dir - ) -> tuple[List[str], List[str]]: + def _combine_components(self, components: list[dict]) -> tuple[list, list, bool]: + """combine a list of components.""" + + all_contents = [] + all_imports = [] + has_chatbot = False + + for component in components: + # Write imports if not already done + component_imports = self._generate_component_imports(component) + all_imports.extend(component_imports) + + # Handle different types of components + fct = self.components_fct_map.get(component.component_type, None) + if fct is None: + self.report.logger.warning( + f"Unsupported component type '{component.component_type}' " + ) + else: + if component.component_type == r.ComponentType.CHATBOT: + has_chatbot = True + content = fct(component) + all_contents.extend(content) + # remove duplicates and isort + all_imports = list(set(all_imports)) + return all_contents, all_imports, has_chatbot + + def _generate_subsection(self, subsection) -> tuple[List[str], List[str]]: """ Generate code to render components (plots, dataframes, markdown) in the given subsection, creating imports and content for the subsection based on the component type. @@ -430,36 +462,10 @@ def _generate_subsection( subsection_content.append( self._format_text(text=subsection.description, type="paragraph") ) - - for component in subsection.components: - # Write imports if not already done - component_imports = self._generate_component_imports(component) - subsection_imports.append(component_imports) - - # Handle different types of components - if component.component_type == r.ComponentType.PLOT: - subsection_content.extend( - self._generate_plot_content(component, static_dir=static_dir) - ) - elif component.component_type == r.ComponentType.DATAFRAME: - subsection_content.extend(self._generate_dataframe_content(component)) - # If md files is called "description.md", do not include it in the report - elif ( - component.component_type == r.ComponentType.MARKDOWN - and component.title.lower() != "description" - ): - subsection_content.extend(self._generate_markdown_content(component)) - elif component.component_type == r.ComponentType.HTML: - subsection_content.extend(self._generate_html_content(component)) - elif component.component_type == r.ComponentType.APICALL: - subsection_content.extend(self._generate_apicall_content(component)) - elif component.component_type == r.ComponentType.CHATBOT: - has_chatbot = True - subsection_content.extend(self._generate_chatbot_content(component)) - else: - self.report.logger.warning( - f"Unsupported component type '{component.component_type}' in subsection: {subsection.title}" - ) + all_components, subsection_imports, has_chatbot = self._combine_components( + subsection.components + ) + subsection_content.extend(all_components) if not has_chatbot: # Define the footer variable and add it to the home page content From fd802ae6bd82aed97ef3efbc3fa93ad86834292b Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Wed, 23 Apr 2025 14:00:57 +0200 Subject: [PATCH 02/15] :art: move static_dir to init, remove some old code --- src/vuegen/report_generator.py | 7 +++-- src/vuegen/streamlit_reportview.py | 50 ++++++++++++++++-------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/vuegen/report_generator.py b/src/vuegen/report_generator.py index 4eedf19..9d813f3 100644 --- a/src/vuegen/report_generator.py +++ b/src/vuegen/report_generator.py @@ -81,9 +81,12 @@ def get_report( sections_dir = report_dir / "sections" static_files_dir = report_dir / "static" st_report = StreamlitReportView( - report=report, report_type=report_type, streamlit_autorun=streamlit_autorun + report=report, + report_type=report_type, + streamlit_autorun=streamlit_autorun, + static_dir=static_files_dir, ) - st_report.generate_report(output_dir=sections_dir, static_dir=static_files_dir) + st_report.generate_report(output_dir=sections_dir) st_report.run_report(output_dir=sections_dir) else: # Check if Quarto is installed diff --git a/src/vuegen/streamlit_reportview.py b/src/vuegen/streamlit_reportview.py index 3fd441a..94b9738 100644 --- a/src/vuegen/streamlit_reportview.py +++ b/src/vuegen/streamlit_reportview.py @@ -37,7 +37,22 @@ def __init__( report: r.Report, report_type: r.ReportType, streamlit_autorun: bool = False, + static_dir: str = STATIC_FILES_DIR, ): + """Initialize ReportView with the report and report type. + + Parameters + ---------- + report : r.Report + Report dataclass with all the information to be included in the report. + Contains sections data needed to write the report python files. + report_type : r.ReportType + Enum of report type as definded by the ReportType Enum. + streamlit_autorun : bool, optional + Wheather streamlit should be started after report generation, by default False + static_dir : str, optional + The folder where the static files will be saved, by default STATIC_FILES_DIR. + """ super().__init__(report=report, report_type=report_type) self.streamlit_autorun = streamlit_autorun self.BUNDLED_EXECUTION = False @@ -56,9 +71,9 @@ def __init__( r.ComponentType.CHATBOT: self._generate_chatbot_content, } - def generate_report( - self, output_dir: str = SECTIONS_DIR, static_dir: str = STATIC_FILES_DIR - ) -> None: + self.static_dir = static_dir + + def generate_report(self, output_dir: str = SECTIONS_DIR) -> None: """ Generates the Streamlit report and creates Python files for each section and its subsections and plots. @@ -66,8 +81,6 @@ def generate_report( ---------- output_dir : str, optional The folder where the generated report files will be saved (default is SECTIONS_DIR). - static_dir : str, optional - The folder where the static files will be saved (default is STATIC_FILES_DIR). """ self.report.logger.debug( f"Generating '{self.report_type}' report in directory: '{output_dir}'" @@ -80,13 +93,13 @@ def generate_report( self.report.logger.info(f"Output directory already existed: '{output_dir}'") # Create the static folder - if create_folder(static_dir): + if create_folder(self.static_dir): self.report.logger.info( - f"Created output directory for static content: '{static_dir}'" + f"Created output directory for static content: '{self.static_dir}'" ) else: self.report.logger.info( - f"Output directory for static content already existed: '{static_dir}'" + f"Output directory for static content already existed: '{self.static_dir}'" ) try: @@ -172,7 +185,7 @@ def generate_report( ) # Create Python files for each section and its subsections and plots - self._generate_sections(output_dir=output_dir, static_dir=static_dir) + self._generate_sections(output_dir=output_dir) except Exception as e: self.report.logger.error( f"An error occurred while generating the report: {str(e)}" @@ -336,7 +349,7 @@ def _generate_home_section( self.report.logger.error(f"Error generating the home section: {str(e)}") raise - def _generate_sections(self, output_dir: str, static_dir: str) -> None: + def _generate_sections(self, output_dir: str) -> None: """ Generates Python files for each section in the report, including subsections and its components (plots, dataframes, markdown). @@ -344,8 +357,6 @@ def _generate_sections(self, output_dir: str, static_dir: str) -> None: ---------- output_dir : str The folder where section files will be saved. - static_dir : str - The folder where the static files will be saved. """ self.report.logger.info("Starting to generate sections for the report.") @@ -366,7 +377,8 @@ def _generate_sections(self, output_dir: str, static_dir: str) -> None: # Iterate through subsections and integrate them into the section file for subsection in section.subsections: self.report.logger.debug( - f"Processing subsection '{subsection.id}': '{subsection.title} - {len(subsection.components)} component(s)'" + f"Processing subsection '{subsection.id}': '{subsection.title} -" + f" {len(subsection.components)} component(s)'" ) try: # Create subsection file @@ -437,8 +449,6 @@ def _generate_subsection(self, subsection) -> tuple[List[str], List[str]]: ---------- subsection : Subsection The subsection containing the components. - static_dir : str - The folder where the static files will be saved. Returns ------- @@ -447,10 +457,6 @@ def _generate_subsection(self, subsection) -> tuple[List[str], List[str]]: - list of imports for the subsection (List[str]) """ subsection_content = [] - subsection_imports = [] - - # Track if there's a Chatbot component in this subsection - has_chatbot = False # Add subsection header and description subsection_content.append( @@ -477,7 +483,7 @@ def _generate_subsection(self, subsection) -> tuple[List[str], List[str]]: ) return subsection_content, subsection_imports - def _generate_plot_content(self, plot, static_dir: str) -> List[str]: + def _generate_plot_content(self, plot) -> List[str]: """ Generate content for a plot component based on the plot type (static or interactive). @@ -490,8 +496,6 @@ def _generate_plot_content(self, plot, static_dir: str) -> List[str]: ------- list : List[str] The list of content lines for the plot. - static_dir : str - The folder where the static files will be saved. """ plot_content = [] # Add title @@ -517,7 +521,7 @@ def _generate_plot_content(self, plot, static_dir: str) -> List[str]: else: # Otherwise, create and save a new pyvis network from the netowrkx graph html_plot_file = ( - Path(static_dir) / f"{plot.title.replace(' ', '_')}.html" + Path(self.static_dir) / f"{plot.title.replace(' ', '_')}.html" ) pyvis_graph = plot.create_and_save_pyvis_network( networkx_graph, html_plot_file From 9e8f17be678e47c8aa5e36009e646821de2e257e Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Wed, 23 Apr 2025 15:06:48 +0200 Subject: [PATCH 03/15] :sparkles: add components to streamlit home site - first section is home section recording it's compents - needs to be treated separately --- src/vuegen/config_manager.py | 19 +++++++++++++++++++ src/vuegen/report.py | 1 + src/vuegen/streamlit_reportview.py | 27 +++++++++++++++++++++++---- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/vuegen/config_manager.py b/src/vuegen/config_manager.py index dac12b4..8330213 100644 --- a/src/vuegen/config_manager.py +++ b/src/vuegen/config_manager.py @@ -301,12 +301,27 @@ def create_yamlconfig_fromdir( # Sort sections by their number prefix sorted_sections = self._sort_paths_by_numprefix(list(base_dir_path.iterdir())) + main_section_config = { + "title": self._create_title_fromdir("home_components"), + "description": "Components added to homepage.", + "components": [], + } + yaml_config["sections"].append(main_section_config) + # Generate sections and subsections config for section_dir in sorted_sections: if section_dir.is_dir(): yaml_config["sections"].append( self._create_sect_config_fromdir(section_dir) ) + # could be single plots? + else: + file_in_main_section_dir = section_dir + component_config = self._create_component_config_fromfile( + file_in_main_section_dir + ) + if component_config is not None: + main_section_config["components"].append(component_config) return yaml_config, base_dir_path @@ -372,6 +387,10 @@ def _create_section(self, section_data: dict) -> r.Section: description=section_data.get("description"), ) + for component_data in section_data.get("components", []): + component = self._create_component(component_data) + section.components.append(component) + # Create subsections for subsection_data in section_data.get("subsections", []): subsection = self._create_subsection(subsection_data) diff --git a/src/vuegen/report.py b/src/vuegen/report.py index 147ec14..eb41fe1 100644 --- a/src/vuegen/report.py +++ b/src/vuegen/report.py @@ -718,6 +718,7 @@ class Section: id: int = field(init=False) title: str subsections: List["Subsection"] = field(default_factory=list) + components: List["Component"] = field(default_factory=list) description: Optional[str] = None def __post_init__(self): diff --git a/src/vuegen/streamlit_reportview.py b/src/vuegen/streamlit_reportview.py index 94b9738..86dfa7d 100644 --- a/src/vuegen/streamlit_reportview.py +++ b/src/vuegen/streamlit_reportview.py @@ -127,11 +127,14 @@ def generate_report(self, output_dir: str = SECTIONS_DIR) -> None: report_manag_content.append("\nsections_pages = {}") # Generate the home page and update the report manager content + # ! top level files (compontents) are added to the home page self._generate_home_section( - output_dir=output_dir, report_manag_content=report_manag_content + output_dir=output_dir, + report_manag_content=report_manag_content, + home_section=self.report.sections[0], ) - for section in self.report.sections: + for section in self.report.sections[1:]: # skip home section components # Create a folder for each section subsection_page_vars = [] section_name_var = section.title.replace(" ", "_") @@ -293,7 +296,10 @@ def _format_text( return f"""st.markdown('''<{tag} style='text-align: {text_align}; color: {color};'>{text}''', unsafe_allow_html=True)""" def _generate_home_section( - self, output_dir: str, report_manag_content: list + self, + output_dir: str, + report_manag_content: list, + home_section: r.Section, ) -> None: """ Generates the homepage for the report and updates the report manager content. @@ -306,6 +312,13 @@ def _generate_home_section( A list to store the content that will be written to the report manager file. """ self.report.logger.debug("Processing home section.") + all_components = [] + subsection_imports = [] + if home_section.components: + # some assert on title? + all_components, subsection_imports, _ = self._combine_components( + home_section.components + ) try: # Create folder for the home page @@ -320,6 +333,8 @@ def _generate_home_section( # Create the home page content home_content = [] home_content.append(f"import streamlit as st") + if subsection_imports: + home_content.extend(subsection_imports) if self.report.description: home_content.append( self._format_text(text=self.report.description, type="paragraph") @@ -329,6 +344,10 @@ def _generate_home_section( f"\nst.image('{self.report.graphical_abstract}', use_column_width=True)" ) + # add components content to page (if any) + if all_components: + home_content.extend(all_components) + # Define the footer variable and add it to the home page content home_content.append("footer = '''" + generate_footer() + "'''\n") home_content.append("st.markdown(footer, unsafe_allow_html=True)\n") @@ -361,7 +380,7 @@ def _generate_sections(self, output_dir: str) -> None: self.report.logger.info("Starting to generate sections for the report.") try: - for section in self.report.sections: + for section in self.report.sections[1:]: section_name_var = section.title.replace(" ", "_") self.report.logger.debug( f"Processing section '{section.id}': '{section.title}' - {len(section.subsections)} subsection(s)" From 6570819d9a2e23b12adc106f5b6975e4967df297 Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Thu, 24 Apr 2025 11:47:23 +0200 Subject: [PATCH 04/15] :sparkles: Add section components - add overview page for each section showing components in main section folder --- src/vuegen/config_manager.py | 11 ++++++++++ src/vuegen/streamlit_reportview.py | 33 +++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/vuegen/config_manager.py b/src/vuegen/config_manager.py index 8330213..7fef611 100644 --- a/src/vuegen/config_manager.py +++ b/src/vuegen/config_manager.py @@ -257,14 +257,25 @@ def _create_sect_config_fromdir( ) subsections = [] + components = [] for subsection_dir in sorted_subsections: if subsection_dir.is_dir(): subsections.append(self._create_subsect_config_fromdir(subsection_dir)) + else: + file_in_subsection_dir = ( + subsection_dir # ! maybe take more generic names? + ) + component_config = self._create_component_config_fromfile( + file_in_subsection_dir + ) + if component_config is not None: + components.append(component_config) section_config = { "title": self._create_title_fromdir(section_dir_path.name), "description": self._read_description_file(section_dir_path), "subsections": subsections, + "components": components, } return section_config diff --git a/src/vuegen/streamlit_reportview.py b/src/vuegen/streamlit_reportview.py index 86dfa7d..c102db8 100644 --- a/src/vuegen/streamlit_reportview.py +++ b/src/vuegen/streamlit_reportview.py @@ -1,5 +1,4 @@ import os -import re import subprocess import sys from pathlib import Path @@ -148,6 +147,18 @@ def generate_report(self, output_dir: str = SECTIONS_DIR) -> None: self.report.logger.debug( f"Section directory already existed: {section_dir_path}" ) + # add an overview page to section of components exist + if section.components: + subsection_file_path = ( + Path(section_name_var) + / f"0_overview_{section.title.lower()}.py" + ).as_posix() # Make sure it's Posix Paths + + # Create a Page object for each subsection and add it to the home page content + report_manag_content.append( + f"{section_name_var}_overview = st.Page('{subsection_file_path}', title='Overview {section.title}')" + ) + subsection_page_vars.append(f"{section_name_var}_overview") for subsection in section.subsections: # ! could add a non-integer to ensure it's a valid identifier @@ -386,6 +397,25 @@ def _generate_sections(self, output_dir: str) -> None: f"Processing section '{section.id}': '{section.title}' - {len(section.subsections)} subsection(s)" ) + if section.components: + # add an section overview page + section_content, section_imports, _ = self._combine_components( + section.components + ) + _filepath_overview = ( + Path(output_dir) + / section_name_var + / f"0_overview_{section.title.lower()}.py" + # ! tighly coupled to generate_report fct: + # ! check how to pass file names + ) + + write_python_file( + fpath=_filepath_overview, + imports=section_imports, + contents=section_content, + ) + if not section.subsections: self.report.logger.warning( f"No subsections found in section: '{section.title}'. " @@ -394,6 +424,7 @@ def _generate_sections(self, output_dir: str) -> None: continue # Iterate through subsections and integrate them into the section file + # subsection should have the subsection_file_path as file_path? for subsection in section.subsections: self.report.logger.debug( f"Processing subsection '{subsection.id}': '{subsection.title} -" From 4eaf9a08a5722cae4fdeebf0e8d95248a8bce82c Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Thu, 24 Apr 2025 11:59:22 +0200 Subject: [PATCH 05/15] :art: pass relative section file paths on using Section dataclasses - Section could be used everywhere, Subsection is not really necessary. If a Section has subsections, the logic changes. Else nothing changes. --- src/vuegen/report.py | 11 ++++++++++ src/vuegen/streamlit_reportview.py | 33 +++++++++++++----------------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/vuegen/report.py b/src/vuegen/report.py index eb41fe1..c3f6625 100644 --- a/src/vuegen/report.py +++ b/src/vuegen/report.py @@ -678,6 +678,9 @@ class Subsection: A list of components within the subsection. description : str, optional A description of the subsection (default is None). + file_path : str, optional + Relative file path to the section file in sections folder. + Used for building reports (default is None). """ _id_counter: ClassVar[int] = 0 @@ -685,6 +688,7 @@ class Subsection: title: str components: List["Component"] = field(default_factory=list) description: Optional[str] = None + file_path: Optional[str] = None def __post_init__(self): self.id = self._generate_id() @@ -695,6 +699,7 @@ def _generate_id(cls) -> int: return cls._id_counter +# ? Section is a subclass of Subsection (adding subsections). Destinction might not be necessary @dataclass class Section: """ @@ -710,8 +715,13 @@ class Section: Title of the section. subsections : List[Subsection] A list of subsections within the section. + components : List[Component] + A list of components within the subsection. description : str, optional A description of the section (default is None). + file_path : str, optional + Relative file path to the section file in sections folder. + Used for building reports (default is None). """ _id_counter: ClassVar[int] = 0 @@ -720,6 +730,7 @@ class Section: subsections: List["Subsection"] = field(default_factory=list) components: List["Component"] = field(default_factory=list) description: Optional[str] = None + file_path: Optional[str] = None def __post_init__(self): self.id = self._generate_id() diff --git a/src/vuegen/streamlit_reportview.py b/src/vuegen/streamlit_reportview.py index c102db8..8526f08 100644 --- a/src/vuegen/streamlit_reportview.py +++ b/src/vuegen/streamlit_reportview.py @@ -136,7 +136,9 @@ def generate_report(self, output_dir: str = SECTIONS_DIR) -> None: for section in self.report.sections[1:]: # skip home section components # Create a folder for each section subsection_page_vars = [] - section_name_var = section.title.replace(" ", "_") + section_name_var = make_valid_identifier( + section.title.replace(" ", "_") + ) section_dir_path = Path(output_dir) / section_name_var if create_folder(section_dir_path): @@ -151,9 +153,9 @@ def generate_report(self, output_dir: str = SECTIONS_DIR) -> None: if section.components: subsection_file_path = ( Path(section_name_var) - / f"0_overview_{section.title.lower()}.py" + / f"0_overview_{make_valid_identifier(section.title).lower()}.py" ).as_posix() # Make sure it's Posix Paths - + section.file_path = subsection_file_path # Create a Page object for each subsection and add it to the home page content report_manag_content.append( f"{section_name_var}_overview = st.Page('{subsection_file_path}', title='Overview {section.title}')" @@ -173,7 +175,7 @@ def generate_report(self, output_dir: str = SECTIONS_DIR) -> None: subsection_file_path = ( Path(section_name_var) / f"{subsection_name_var}.py" ).as_posix() # Make sure it's Posix Paths - + subsection.file_path = subsection_file_path # Create a Page object for each subsection and add it to the home page content report_manag_content.append( f"{subsection_name_var} = st.Page('{subsection_file_path}', title='{subsection.title}')" @@ -402,16 +404,11 @@ def _generate_sections(self, output_dir: str) -> None: section_content, section_imports, _ = self._combine_components( section.components ) - _filepath_overview = ( - Path(output_dir) - / section_name_var - / f"0_overview_{section.title.lower()}.py" - # ! tighly coupled to generate_report fct: - # ! check how to pass file names - ) - + assert ( + section.file_path is not None + ), "Missing relative file path to overview page in section" write_python_file( - fpath=_filepath_overview, + fpath=Path(output_dir) / section.file_path, imports=section_imports, contents=section_content, ) @@ -433,12 +430,10 @@ def _generate_sections(self, output_dir: str) -> None: try: # Create subsection file _subsection_name = make_valid_identifier(subsection.title) - subsection_file_path = ( - Path(output_dir) - / section_name_var - / f"{_subsection_name}.py" - ) - + assert ( + subsection.file_path is not None + ), "Missing relative file path to subsection" + subsection_file_path = Path(output_dir) / subsection.file_path # Generate content and imports for the subsection subsection_content, subsection_imports = ( self._generate_subsection(subsection) From b43b24853b7679c085e27115ea6007e5fa072cbb Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Fri, 25 Apr 2025 14:35:14 +0200 Subject: [PATCH 06/15] :fire: remove old code (unused) --- src/vuegen/quarto_reportview.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index 9595f95..56a6c52 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -1,4 +1,3 @@ -import logging import os import subprocess import sys @@ -28,12 +27,12 @@ def __init__( ): super().__init__(report=report, report_type=report_type) self.quarto_checks = quarto_checks - self.BUNDLED_EXECUTION = False + # self.BUNDLED_EXECUTION = False self.quarto_path = "quarto" # self.env_vars = os.environ.copy() if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): self.report.logger.info("running in a PyInstaller bundle") - self.BUNDLED_EXECUTION = True + # self.BUNDLED_EXECUTION = True self.report.logger.debug(f"sys._MEIPASS: {sys._MEIPASS}") else: self.report.logger.info("running in a normal Python process") From d473f6d19cb128d71d7e3d163b31dbde172996b7 Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Fri, 25 Apr 2025 14:39:20 +0200 Subject: [PATCH 07/15] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20move=20is=5Fstatic?= =?UTF-8?q?=5Freport=20to=20init=20for=20quarto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - set based on report type, and this is set on init - avoid to pass on variable to all functions. --- src/vuegen/quarto_reportview.py | 57 ++++++++++++--------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index 56a6c52..2f0119e 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -41,6 +41,13 @@ def __init__( self.report.logger.debug(f"PATH: {os.environ['PATH']}") self.report.logger.debug(f"sys.path: {sys.path}") + self.is_report_static = self.report_type in { + r.ReportType.PDF, + r.ReportType.DOCX, + r.ReportType.ODT, + r.ReportType.PPTX, + } + def generate_report( self, output_dir: Path = BASE_DIR, static_dir: Path = STATIC_FILES_DIR ) -> None: @@ -78,12 +85,6 @@ def generate_report( try: # Create variable to check if the report is static or revealjs - is_report_static = self.report_type in { - r.ReportType.PDF, - r.ReportType.DOCX, - r.ReportType.ODT, - r.ReportType.PPTX, - } is_report_revealjs = self.report_type == r.ReportType.REVEALJS # Define the YAML header for the quarto report @@ -123,7 +124,6 @@ def generate_report( subsection_content, subsection_imports = ( self._generate_subsection( subsection, - is_report_static, is_report_revealjs, static_dir=static_dir, ) @@ -391,7 +391,6 @@ def _create_yaml_header(self) -> str: def _generate_subsection( self, subsection, - is_report_static, is_report_revealjs, static_dir: str, ) -> tuple[List[str], List[str]]: @@ -403,8 +402,6 @@ def _generate_subsection( ---------- subsection : Subsection The subsection containing the components. - is_report_static : bool - A boolean indicating whether the report is static or interactive. is_report_revealjs : bool A boolean indicating whether the report is in revealjs format. static_dir : str @@ -432,15 +429,11 @@ def _generate_subsection( if component.component_type == r.ComponentType.PLOT: subsection_content.extend( - self._generate_plot_content( - component, is_report_static, static_dir=static_dir - ) + self._generate_plot_content(component, static_dir=static_dir) ) elif component.component_type == r.ComponentType.DATAFRAME: subsection_content.extend( - self._generate_dataframe_content( - component, is_report_static, static_dir=static_dir - ) + self._generate_dataframe_content(component, static_dir=static_dir) ) elif ( component.component_type == r.ComponentType.MARKDOWN @@ -449,7 +442,7 @@ def _generate_subsection( subsection_content.extend(self._generate_markdown_content(component)) elif ( component.component_type == r.ComponentType.HTML - and not is_report_static + and not self.is_report_static ): subsection_content.extend(self._generate_html_content(component)) else: @@ -465,9 +458,7 @@ def _generate_subsection( ) return subsection_content, subsection_imports - def _generate_plot_content( - self, plot, is_report_static, static_dir: str - ) -> List[str]: + def _generate_plot_content(self, plot, static_dir: str) -> List[str]: """ Generate content for a plot component based on the report type. @@ -488,7 +479,7 @@ def _generate_plot_content( plot_content.append(f"### {plot.title}") # Define plot path - if is_report_static: + if self.is_report_static: static_plot_path = Path(static_dir) / f"{plot.title.replace(' ', '_')}.png" else: html_plot_file = Path(static_dir) / f"{plot.title.replace(' ', '_')}.html" @@ -501,7 +492,7 @@ def _generate_plot_content( ) elif plot.plot_type == r.PlotType.PLOTLY: plot_content.append(self._generate_plot_code(plot)) - if is_report_static: + if self.is_report_static: plot_content.append( f"""fig_plotly.write_image("{static_plot_path.resolve().as_posix()}")\n```\n""" ) @@ -510,7 +501,7 @@ def _generate_plot_content( plot_content.append(f"""fig_plotly.show()\n```\n""") elif plot.plot_type == r.PlotType.ALTAIR: plot_content.append(self._generate_plot_code(plot)) - if is_report_static: + if self.is_report_static: plot_content.append( f"""fig_altair.save("{static_plot_path.resolve().as_posix()}")\n```\n""" ) @@ -522,7 +513,7 @@ def _generate_plot_content( if isinstance(networkx_graph, tuple): # If network_data is a tuple, separate the network and html file path networkx_graph, html_plot_file = networkx_graph - elif isinstance(networkx_graph, nx.Graph) and not is_report_static: + elif isinstance(networkx_graph, nx.Graph) and not self.is_report_static: # Get the pyvis object and create html pyvis_graph = plot.create_and_save_pyvis_network( networkx_graph, html_plot_file @@ -535,7 +526,7 @@ def _generate_plot_content( plot_content.append(f"**Number of edges:** {num_edges}\n") # Add code to generate network depending on the report type - if is_report_static: + if self.is_report_static: plot.save_network_image(networkx_graph, static_plot_path, "png") plot_content.append(self._generate_image_content(static_plot_path)) else: @@ -619,9 +610,7 @@ def _generate_plot_code(self, plot, output_file="") -> str: \n""" return plot_code - def _generate_dataframe_content( - self, dataframe, is_report_static, static_dir: str - ) -> List[str]: + def _generate_dataframe_content(self, dataframe, static_dir: str) -> List[str]: """ Generate content for a DataFrame component based on the report type. @@ -629,8 +618,6 @@ def _generate_dataframe_content( ---------- dataframe : DataFrame The dataframe component to add to content. - is_report_static : bool - A boolean indicating whether the report is static or interactive. static_dir : str The folder where the static files will be saved. @@ -683,7 +670,7 @@ def _generate_dataframe_content( # Display the dataframe dataframe_content.extend( - self._show_dataframe(dataframe, is_report_static, static_dir=static_dir) + self._show_dataframe(dataframe, static_dir=static_dir) ) except Exception as e: @@ -835,9 +822,7 @@ def _generate_image_content( f"""![](/{src}){{fig-alt={alt_text} width={width} height={height}}}\n""" ) - def _show_dataframe( - self, dataframe, is_report_static, static_dir: str - ) -> List[str]: + def _show_dataframe(self, dataframe, static_dir: str) -> List[str]: """ Appends either a static image or an interactive representation of a DataFrame to the content list. @@ -845,8 +830,6 @@ def _show_dataframe( ---------- dataframe : DataFrame The DataFrame object containing the data to display. - is_report_static : bool - Determines if the report is in a static format (e.g., PDF) or interactive (e.g., HTML). static_dir : str The folder where the static files will be saved. @@ -856,7 +839,7 @@ def _show_dataframe( The list of content lines for the DataFrame. """ dataframe_content = [] - if is_report_static: + if self.is_report_static: # Generate path for the DataFrame image df_image = Path(static_dir) / f"{dataframe.title.replace(' ', '_')}.png" dataframe_content.append( From 8c20abd7eb097258c40055b3b9096476fa689c3b Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Fri, 25 Apr 2025 14:51:53 +0200 Subject: [PATCH 08/15] :art: move static_dir to quarto_report init - same as for streamlit_report to prepare refactoring of components parsing. --- src/vuegen/quarto_reportview.py | 69 +++++++++++++++++---------------- src/vuegen/report_generator.py | 9 +++-- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index 2f0119e..e29ed30 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -24,9 +24,25 @@ def __init__( report: r.Report, report_type: r.ReportType, quarto_checks: bool = False, + static_dir: str = STATIC_FILES_DIR, ): + """_summary_ + + Parameters + ---------- + report : r.Report + Report dataclass with all the information to be included in the report. + Contains sections data needed to write the report python files. + report_type : r.ReportType + Enum of report type as definded by the ReportType Enum. + quarto_checks : bool, optional + Whether to test if all quarto dependencies are installed, by default False + static_dir : str + The folder where the static files will be saved. + """ super().__init__(report=report, report_type=report_type) self.quarto_checks = quarto_checks + self.static_dir = static_dir # self.BUNDLED_EXECUTION = False self.quarto_path = "quarto" # self.env_vars = os.environ.copy() @@ -48,9 +64,7 @@ def __init__( r.ReportType.PPTX, } - def generate_report( - self, output_dir: Path = BASE_DIR, static_dir: Path = STATIC_FILES_DIR - ) -> None: + def generate_report(self, output_dir: Path = BASE_DIR) -> None: """ Generates the qmd file of the quarto report. It creates code for rendering each section and its subsections with all components. @@ -58,8 +72,6 @@ def generate_report( ---------- output_dir : Path, optional The folder where the generated report files will be saved (default is BASE_DIR). - static_dir : Path, optional - The folder where the static files will be saved (default is STATIC_FILES_DIR). """ self.report.logger.debug( f"Generating '{self.report_type}' report in directory: '{output_dir}'" @@ -74,13 +86,13 @@ def generate_report( ) # Create the static folder - if create_folder(static_dir): + if create_folder(self.static_dir): self.report.logger.info( - f"Created output directory for static content: '{static_dir}'" + f"Created output directory for static content: '{self.static_dir}'" ) else: self.report.logger.info( - f"Output directory for static content already existed: '{static_dir}'" + f"Output directory for static content already existed: '{self.static_dir}'" ) try: @@ -125,7 +137,6 @@ def generate_report( self._generate_subsection( subsection, is_report_revealjs, - static_dir=static_dir, ) ) qmd_content.extend(subsection_content) @@ -392,7 +403,6 @@ def _generate_subsection( self, subsection, is_report_revealjs, - static_dir: str, ) -> tuple[List[str], List[str]]: """ Generate code to render components (plots, dataframes, markdown) in the given subsection, @@ -404,8 +414,7 @@ def _generate_subsection( The subsection containing the components. is_report_revealjs : bool A boolean indicating whether the report is in revealjs format. - static_dir : str - The folder where the static files will be saved. + Returns ------- tuple : (List[str], List[str]) @@ -428,13 +437,9 @@ def _generate_subsection( subsection_imports.append(component_imports) if component.component_type == r.ComponentType.PLOT: - subsection_content.extend( - self._generate_plot_content(component, static_dir=static_dir) - ) + subsection_content.extend(self._generate_plot_content(component)) elif component.component_type == r.ComponentType.DATAFRAME: - subsection_content.extend( - self._generate_dataframe_content(component, static_dir=static_dir) - ) + subsection_content.extend(self._generate_dataframe_content(component)) elif ( component.component_type == r.ComponentType.MARKDOWN and component.title.lower() != "description" @@ -458,7 +463,7 @@ def _generate_subsection( ) return subsection_content, subsection_imports - def _generate_plot_content(self, plot, static_dir: str) -> List[str]: + def _generate_plot_content(self, plot) -> List[str]: """ Generate content for a plot component based on the report type. @@ -466,8 +471,6 @@ def _generate_plot_content(self, plot, static_dir: str) -> List[str]: ---------- plot : Plot The plot component to generate content for. - static_dir : str - The folder where the static files will be saved. Returns ------- @@ -480,9 +483,13 @@ def _generate_plot_content(self, plot, static_dir: str) -> List[str]: # Define plot path if self.is_report_static: - static_plot_path = Path(static_dir) / f"{plot.title.replace(' ', '_')}.png" + static_plot_path = ( + Path(self.static_dir) / f"{plot.title.replace(' ', '_')}.png" + ) else: - html_plot_file = Path(static_dir) / f"{plot.title.replace(' ', '_')}.html" + html_plot_file = ( + Path(self.static_dir) / f"{plot.title.replace(' ', '_')}.html" + ) # Add content for the different plot types try: @@ -610,7 +617,7 @@ def _generate_plot_code(self, plot, output_file="") -> str: \n""" return plot_code - def _generate_dataframe_content(self, dataframe, static_dir: str) -> List[str]: + def _generate_dataframe_content(self, dataframe) -> List[str]: """ Generate content for a DataFrame component based on the report type. @@ -618,8 +625,6 @@ def _generate_dataframe_content(self, dataframe, static_dir: str) -> List[str]: ---------- dataframe : DataFrame The dataframe component to add to content. - static_dir : str - The folder where the static files will be saved. Returns ------- @@ -669,9 +674,7 @@ def _generate_dataframe_content(self, dataframe, static_dir: str) -> List[str]: ) # Display the dataframe - dataframe_content.extend( - self._show_dataframe(dataframe, static_dir=static_dir) - ) + dataframe_content.extend(self._show_dataframe(dataframe)) except Exception as e: self.report.logger.error( @@ -822,7 +825,7 @@ def _generate_image_content( f"""![](/{src}){{fig-alt={alt_text} width={width} height={height}}}\n""" ) - def _show_dataframe(self, dataframe, static_dir: str) -> List[str]: + def _show_dataframe(self, dataframe) -> List[str]: """ Appends either a static image or an interactive representation of a DataFrame to the content list. @@ -830,8 +833,6 @@ def _show_dataframe(self, dataframe, static_dir: str) -> List[str]: ---------- dataframe : DataFrame The DataFrame object containing the data to display. - static_dir : str - The folder where the static files will be saved. Returns ------- @@ -841,7 +842,9 @@ def _show_dataframe(self, dataframe, static_dir: str) -> List[str]: dataframe_content = [] if self.is_report_static: # Generate path for the DataFrame image - df_image = Path(static_dir) / f"{dataframe.title.replace(' ', '_')}.png" + df_image = ( + Path(self.static_dir) / f"{dataframe.title.replace(' ', '_')}.png" + ) dataframe_content.append( f"df.dfi.export('{Path(df_image).resolve().as_posix()}', max_rows=10, max_cols=5, table_conversion='matplotlib')\n```\n" ) diff --git a/src/vuegen/report_generator.py b/src/vuegen/report_generator.py index 9d813f3..7708571 100644 --- a/src/vuegen/report_generator.py +++ b/src/vuegen/report_generator.py @@ -102,11 +102,12 @@ def get_report( report_dir = output_dir / "quarto_report" static_files_dir = report_dir / "static" quarto_report = QuartoReportView( - report=report, report_type=report_type, quarto_checks=quarto_checks - ) - quarto_report.generate_report( - output_dir=report_dir, static_dir=static_files_dir + report=report, + report_type=report_type, + quarto_checks=quarto_checks, + static_dir=static_files_dir, ) + quarto_report.generate_report(output_dir=report_dir) quarto_report.run_report(output_dir=report_dir) # ? Could be also the path to the report file for quarto based reports return report_dir, config_path From 30b531c6314f07ddbe3fd994e1d9e0af4646931f Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Fri, 25 Apr 2025 15:28:30 +0200 Subject: [PATCH 09/15] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20factor=20out=20compo?= =?UTF-8?q?nent=20creation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - very similar to _combine_components in streamlit_report - imports has to be a list of list -> figure this out - added debug message for skipping components in static reports-> should maybe be a warining? --- src/vuegen/quarto_reportview.py | 70 ++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index e29ed30..e2992c7 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -64,6 +64,13 @@ def __init__( r.ReportType.PPTX, } + self.components_fct_map = { + r.ComponentType.PLOT: self._generate_plot_content, + r.ComponentType.DATAFRAME: self._generate_dataframe_content, + r.ComponentType.MARKDOWN: self._generate_markdown_content, + r.ComponentType.HTML: self._generate_html_content, + } + def generate_report(self, output_dir: Path = BASE_DIR) -> None: """ Generates the qmd file of the quarto report. It creates code for rendering each section and its subsections with all components. @@ -399,6 +406,39 @@ def _create_yaml_header(self) -> str: return yaml_header + def _combine_components(self, components: list[dict]) -> tuple[list, list]: + """combine a list of components.""" + + all_contents = [] + all_imports = [] + + for component in components: + # Write imports if not already done + component_imports = self._generate_component_imports(component) + self.report.logger.debug("component_imports: %s", component_imports) + all_imports.append(component_imports) # ! different than for streamlit + + # Handle different types of components + fct = self.components_fct_map.get(component.component_type, None) + if fct is None: + self.report.logger.warning( + f"Unsupported component type '{component.component_type}' " + ) + elif ( + component.component_type == r.ComponentType.MARKDOWN + and component.title.lower() == "description" + ): + self.report.logger.debug("Skipping description.md markdown of section.") + elif ( + component.component_type == r.ComponentType.HTML + and self.is_report_static + ): + self.report.logger.debug("Skipping HTML component for static report.") + else: + content = fct(component) + all_contents.extend(content) + return all_contents, all_imports + def _generate_subsection( self, subsection, @@ -422,7 +462,6 @@ def _generate_subsection( - list of imports for the subsection (List[str]) """ subsection_content = [] - subsection_imports = [] # Add subsection header and description subsection_content.append(f"## {subsection.title}") @@ -430,30 +469,13 @@ def _generate_subsection( subsection_content.append(f"""{subsection.description}\n""") if is_report_revealjs: - subsection_content.append(f"::: {{.panel-tabset}}\n") - - for component in subsection.components: - component_imports = self._generate_component_imports(component) - subsection_imports.append(component_imports) + subsection_content.append("::: {{.panel-tabset}}\n") - if component.component_type == r.ComponentType.PLOT: - subsection_content.extend(self._generate_plot_content(component)) - elif component.component_type == r.ComponentType.DATAFRAME: - subsection_content.extend(self._generate_dataframe_content(component)) - elif ( - component.component_type == r.ComponentType.MARKDOWN - and component.title.lower() != "description" - ): - subsection_content.extend(self._generate_markdown_content(component)) - elif ( - component.component_type == r.ComponentType.HTML - and not self.is_report_static - ): - subsection_content.extend(self._generate_html_content(component)) - else: - self.report.logger.warning( - f"Unsupported component type '{component.component_type}' in subsection: {subsection.title}" - ) + ( + all_components, + subsection_imports, + ) = self._combine_components(subsection.components) + subsection_content.extend(all_components) if is_report_revealjs: subsection_content.append(":::\n") From 8129c74f45bef3f66944894b7190216773349c93 Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Mon, 28 Apr 2025 10:41:02 +0200 Subject: [PATCH 10/15] :art: remove blank line from logs --- src/vuegen/config_manager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vuegen/config_manager.py b/src/vuegen/config_manager.py index 7fef611..25fa86a 100644 --- a/src/vuegen/config_manager.py +++ b/src/vuegen/config_manager.py @@ -145,7 +145,7 @@ def _create_component_config_fromfile(self, file_path: Path) -> Dict[str, str]: component_config["component_type"] = r.ComponentType.MARKDOWN.value else: self.logger.error( - f"Unsupported file extension: {file_ext}. Skipping file: {file_path}\n" + f"Unsupported file extension: {file_ext}. Skipping file: {file_path}" ) return None @@ -227,6 +227,9 @@ def _create_subsect_config_fromdir( continue # Add component config to list components.append(component_config) + # ! if folder go into folder and pull files out? + # nesting level already at point 2 + # loop of components in a folder subsection_config = { "title": self._create_title_fromdir(subsection_dir_path.name), @@ -313,7 +316,7 @@ def create_yamlconfig_fromdir( sorted_sections = self._sort_paths_by_numprefix(list(base_dir_path.iterdir())) main_section_config = { - "title": self._create_title_fromdir("home_components"), + "title": "", # self._create_title_fromdir("home_components"), "description": "Components added to homepage.", "components": [], } From aa31b8143ab2d68c03910975d3f1428b88f24a7e Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Mon, 28 Apr 2025 10:58:36 +0200 Subject: [PATCH 11/15] :sparkles: add components from main and section folders --- src/vuegen/quarto_reportview.py | 36 ++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index e2992c7..b074791 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -111,7 +111,9 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None: # Create qmd content and imports for the report qmd_content = [] - report_imports = [] + report_imports = ( + [] + ) # only one global import list for a single report (different to streamlit) # Add description of the report if self.report.description: @@ -122,9 +124,23 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None: qmd_content.append( self._generate_image_content(self.report.graphical_abstract) ) + # ? Do we need to handle overview separately? + main_section = self.report.sections[0] + + if main_section.components: + self.report.logger.debug( + "Adding components of main section folder to the report as overall overview." + ) + qmd_content.append("# General Overview") + section_content, section_imports = self._combine_components( + main_section.components + ) + qmd_content.extend(section_content) + report_imports.extend(section_imports) + # Add the sections and subsections to the report self.report.logger.info("Starting to generate sections for the report.") - for section in self.report.sections: + for section in self.report.sections[1:]: self.report.logger.debug( f"Processing section: '{section.title}' - {len(section.subsections)} subsection(s)" ) @@ -133,6 +149,18 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None: if section.description: qmd_content.append(f"""{section.description}\n""") + # Add components of section to the report + if section.components: + self.report.logger.debug( + "Adding components of section folder to the report." + ) + qmd_content.append(f"## Overview {section.title}".strip()) + section_content, section_imports = self._combine_components( + section.components + ) + qmd_content.extend(section_content) + report_imports.extend(section_imports) + if section.subsections: # Iterate through subsections and integrate them into the section file for subsection in section.subsections: @@ -147,7 +175,9 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None: ) ) qmd_content.extend(subsection_content) - report_imports.extend(subsection_imports) + report_imports.extend( + subsection_imports + ) # even easier as it's global else: self.report.logger.warning( f"No subsections found in section: '{section.title}'. To show content in the report, add subsections to the section." From 118ec8ccc21094b01ad8c3f191b20fda6b1acb06 Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Mon, 28 Apr 2025 11:37:10 +0200 Subject: [PATCH 12/15] :art: align import handling in streamlit and quarto reports --- src/vuegen/quarto_reportview.py | 11 ++++------- src/vuegen/streamlit_reportview.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index b074791..92e1dac 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -183,13 +183,8 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None: f"No subsections found in section: '{section.title}'. To show content in the report, add subsections to the section." ) - # Flatten the subsection_imports into a single list - flattened_report_imports = [ - imp for sublist in report_imports for imp in sublist - ] - # Remove duplicated imports - report_unique_imports = set(flattened_report_imports) + report_unique_imports = set(report_imports) # ! set leads to random import order # ! separate and sort import statements, separate from setup code @@ -446,7 +441,7 @@ def _combine_components(self, components: list[dict]) -> tuple[list, list]: # Write imports if not already done component_imports = self._generate_component_imports(component) self.report.logger.debug("component_imports: %s", component_imports) - all_imports.append(component_imports) # ! different than for streamlit + all_imports.extend(component_imports) # Handle different types of components fct = self.components_fct_map.get(component.component_type, None) @@ -467,6 +462,8 @@ def _combine_components(self, components: list[dict]) -> tuple[list, list]: else: content = fct(component) all_contents.extend(content) + # remove duplicates + all_imports = list(set(all_imports)) return all_contents, all_imports def _generate_subsection( diff --git a/src/vuegen/streamlit_reportview.py b/src/vuegen/streamlit_reportview.py index 8526f08..8018ecc 100644 --- a/src/vuegen/streamlit_reportview.py +++ b/src/vuegen/streamlit_reportview.py @@ -481,7 +481,7 @@ def _combine_components(self, components: list[dict]) -> tuple[list, list, bool] has_chatbot = True content = fct(component) all_contents.extend(content) - # remove duplicates and isort + # remove duplicates all_imports = list(set(all_imports)) return all_contents, all_imports, has_chatbot From 5ec69b554eb411b798b3d4f550f5dc467eb2e364 Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Tue, 29 Apr 2025 16:56:20 +0200 Subject: [PATCH 13/15] :bug: single curley-brackets - I removed an empty f-string and this lead to doubled curely brackets... --- src/vuegen/quarto_reportview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index 92e1dac..35c32b2 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -496,7 +496,7 @@ def _generate_subsection( subsection_content.append(f"""{subsection.description}\n""") if is_report_revealjs: - subsection_content.append("::: {{.panel-tabset}}\n") + subsection_content.append("::: {.panel-tabset}\n") ( all_components, From e013038ef2190b9daec6d7ec9fe8616e53bacf20 Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Tue, 29 Apr 2025 16:57:18 +0200 Subject: [PATCH 14/15] :art: add tabs to overview sections --- src/vuegen/quarto_reportview.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index 35c32b2..6716ac3 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -132,10 +132,14 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None: "Adding components of main section folder to the report as overall overview." ) qmd_content.append("# General Overview") + if is_report_revealjs: + qmd_content.append("::: {.panel-tabset}\n") section_content, section_imports = self._combine_components( main_section.components ) qmd_content.extend(section_content) + if is_report_revealjs: + qmd_content.append(":::\n") report_imports.extend(section_imports) # Add the sections and subsections to the report @@ -155,11 +159,16 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None: "Adding components of section folder to the report." ) qmd_content.append(f"## Overview {section.title}".strip()) + + if is_report_revealjs: + qmd_content.append("::: {.panel-tabset}\n") section_content, section_imports = self._combine_components( section.components ) qmd_content.extend(section_content) report_imports.extend(section_imports) + if is_report_revealjs: + qmd_content.append(":::\n") if section.subsections: # Iterate through subsections and integrate them into the section file From b9583ecb44387d21a350cf6b914f655d9e0e3967 Mon Sep 17 00:00:00 2001 From: Henry Webel Date: Wed, 30 Apr 2025 15:04:07 +0200 Subject: [PATCH 15/15] :bug: handle single descripton.md files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️ duplicated code - description files are parsed by configuration_manager - skipped for section content. So a section can have content, which is not used - a single description.md file --- src/vuegen/quarto_reportview.py | 41 ++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index 6716ac3..af18eff 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -127,19 +127,27 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None: # ? Do we need to handle overview separately? main_section = self.report.sections[0] + # ! description can be a Markdown component, but it is treated differently + # ! It won't be added to the section content. if main_section.components: self.report.logger.debug( "Adding components of main section folder to the report as overall overview." ) - qmd_content.append("# General Overview") - if is_report_revealjs: - qmd_content.append("::: {.panel-tabset}\n") section_content, section_imports = self._combine_components( main_section.components ) - qmd_content.extend(section_content) - if is_report_revealjs: - qmd_content.append(":::\n") + if section_content: + qmd_content.append("# General Overview") + + if is_report_revealjs: + # Add tabset for revealjs + section_content = [ + "::: {.panel-tabset}\n", + *section_content, + ":::", + ] + qmd_content.extend(section_content) + report_imports.extend(section_imports) # Add the sections and subsections to the report @@ -154,21 +162,28 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None: qmd_content.append(f"""{section.description}\n""") # Add components of section to the report + # ! description can be a Markdown component, but it is treated differently + # ! It won't be added to the section content. if section.components: self.report.logger.debug( "Adding components of section folder to the report." ) - qmd_content.append(f"## Overview {section.title}".strip()) - - if is_report_revealjs: - qmd_content.append("::: {.panel-tabset}\n") section_content, section_imports = self._combine_components( section.components ) - qmd_content.extend(section_content) + if section_content: + qmd_content.append(f"## Overview {section.title}".strip()) + + if is_report_revealjs: + # Add tabset for revealjs + section_content = [ + "::: {.panel-tabset}\n", + *section_content, + ":::", + ] + qmd_content.extend(section_content) + report_imports.extend(section_imports) - if is_report_revealjs: - qmd_content.append(":::\n") if section.subsections: # Iterate through subsections and integrate them into the section file