diff --git a/docs/example_data/Earth_microbiome_vuegen_demo_notebook/Earth_microbiome_vuegen_demo_notebook_config.yaml b/docs/example_data/Earth_microbiome_vuegen_demo_notebook/Earth_microbiome_vuegen_demo_notebook_config.yaml deleted file mode 100644 index c7847d5..0000000 --- a/docs/example_data/Earth_microbiome_vuegen_demo_notebook/Earth_microbiome_vuegen_demo_notebook_config.yaml +++ /dev/null @@ -1,140 +0,0 @@ -report: - title: Earth Microbiome Vuegen Demo Notebook - description: "The Earth Microbiome Project (EMP) is a systematic attempt to characterize\ - \ global microbial taxonomic and functional diversity for the benefit of the planet\ - \ and humankind. \n It aimed to sample the Earth\u2019s microbial communities\ - \ at an unprecedented scale in order to advance our understanding of the organizing\ - \ biogeographic principles that govern microbial community structure. \n The\ - \ EMP dataset is generated from samples that individual researchers have compiled\ - \ and contributed to the EMP. \n The result is both a reference database giving\ - \ global context to DNA sequence data and a framework for incorporating data from\ - \ future studies, fostering increasingly complete characterization of Earth\u2019\ - s microbial diversity.\n \n You can find more information about the Earth Microbiome\ - \ Project at https://earthmicrobiome.org/ and in the [original article](https://www.nature.com/articles/nature24621).\n" - graphical_abstract: '' - logo: '' -sections: -- title: Exploratory Data Analysis - description: '' - subsections: - - title: Sample Exploration - description: '' - components: - - title: Metadata Random Subset - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/1_Exploratory_data_analysis/1_sample_exploration/1_metadata_random_subset.csv - description: '' - caption: '' - component_type: DATAFRAME - file_format: CSV - delimiter: ',' - - title: Animal Samples Map - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/1_Exploratory_data_analysis/1_sample_exploration/2_animal_samples_map.png - description: '' - caption: '' - component_type: PLOT - plot_type: STATIC - - title: Plant Samples Map - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/1_Exploratory_data_analysis/1_sample_exploration/3_plant_samples_map.json - description: '' - caption: '' - component_type: PLOT - plot_type: PLOTLY - - title: Saline Samples Map - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/1_Exploratory_data_analysis/1_sample_exploration/4_saline_samples_map.json - description: '' - caption: '' - component_type: PLOT - plot_type: ALTAIR -- title: Metagenomics - description: '' - subsections: - - title: Alpha Diversity - description: '' - components: - - title: Alpha Diversity Host Associated Samples - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/2_Metagenomics/1_alpha_diversity/1_alpha_diversity_host_associated_samples.png - description: '' - caption: '' - component_type: PLOT - plot_type: STATIC - - title: Alpha Diversity Free Living Samples - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/2_Metagenomics/1_alpha_diversity/2_alpha_diversity_free_living_samples.json - description: '' - caption: '' - component_type: PLOT - plot_type: PLOTLY - - title: Average Copy Number - description: '' - components: - - title: Average Copy Number Emp Ontology Level2 - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/2_Metagenomics/2_average_copy_number/1_average_copy_number_emp_ontology_level2.png - description: '' - caption: '' - component_type: PLOT - plot_type: STATIC - - title: Average Copy Number Emp Ontology Level3 - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/2_Metagenomics/2_average_copy_number/2_average_copy_number_emp_ontology_level3.json - description: '' - caption: '' - component_type: PLOT - plot_type: PLOTLY - - title: Nestedness - description: '' - components: - - title: Nestedness Random Subset - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/2_Metagenomics/3_nestedness/1_nestedness_random_subset.csv - description: '' - caption: '' - component_type: DATAFRAME - file_format: CSV - delimiter: ',' - - title: All Samples - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/2_Metagenomics/3_nestedness/2_all_samples.json - description: '' - caption: '' - component_type: PLOT - plot_type: PLOTLY - - title: Plant Samples - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/2_Metagenomics/3_nestedness/3_plant_samples.json - description: '' - caption: '' - component_type: PLOT - plot_type: PLOTLY - - title: Animal Samples - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/2_Metagenomics/3_nestedness/4_animal_samples.png - description: '' - caption: '' - component_type: PLOT - plot_type: STATIC - - title: Non Saline Samples - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/2_Metagenomics/3_nestedness/5_non_saline_samples.png - description: '' - caption: '' - component_type: PLOT - plot_type: STATIC -- title: Network Analysis - description: '' - subsections: - - title: Phyla Association Networks - description: '' - components: - - title: Phyla Counts Subset - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/3_Network_analysis/1_phyla_association_networks/1_phyla_counts_subset.csv - description: '' - caption: '' - component_type: DATAFRAME - file_format: CSV - delimiter: ',' - - title: Phyla Correlation Network With 0.5 Threshold Edgelist - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/3_Network_analysis/1_phyla_association_networks/2_phyla_correlation_network_with_0.5_threshold_edgelist.csv - description: '' - caption: '' - component_type: PLOT - plot_type: INTERACTIVE_NETWORK - csv_network_format: EDGELIST - - title: Phyla Correlation Network With 0.5 Threshold - file_path: example_data/Earth_microbiome_vuegen_demo_notebook/3_Network_analysis/1_phyla_association_networks/3_phyla_correlation_network_with_0.5_threshold.png - description: '' - caption: '' - component_type: PLOT - plot_type: STATIC diff --git a/src/vuegen/config_manager.py b/src/vuegen/config_manager.py index 4832b08..f671601 100644 --- a/src/vuegen/config_manager.py +++ b/src/vuegen/config_manager.py @@ -63,7 +63,9 @@ def _create_component_config_fromfile(self, file_path: Path) -> Dict[str, str]: # Add title, file path, and description component_config["title"] = self._create_title_fromdir(file_path.name) - component_config["file_path"] = str(file_path.resolve()) + component_config["file_path"] = ( + file_path.resolve().as_posix() + ) # ! needs to be posix for all OS support component_config["description"] = "" component_config["caption"] = "" diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index 600174d..1dfd75d 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -16,7 +16,7 @@ class QuartoReportView(r.ReportView): """ BASE_DIR = "quarto_report" - STATIC_FILES_DIR = os.path.join(BASE_DIR, "static") + STATIC_FILES_DIR = Path(BASE_DIR) / "static" def __init__(self, report: r.Report, report_type: r.ReportType): super().__init__(report=report, report_type=report_type) @@ -124,9 +124,7 @@ def generate_report( report_formatted_imports = "\n".join(report_unique_imports) # Write the navigation and general content to a Python file - with open( - os.path.join(output_dir, f"{self.BASE_DIR}.qmd"), "w" - ) as quarto_report: + with open(Path(output_dir) / f"{self.BASE_DIR}.qmd", "w") as quarto_report: quarto_report.write(yaml_header) quarto_report.write( f"""\n```{{python}} @@ -156,7 +154,7 @@ def run_report(self, output_dir: str = BASE_DIR) -> None: """ try: subprocess.run( - ["quarto", "render", os.path.join(output_dir, f"{self.BASE_DIR}.qmd")], + ["quarto", "render", Path(output_dir) / f"{self.BASE_DIR}.qmd"], check=True, ) if self.report_type == r.ReportType.JUPYTER: @@ -164,7 +162,7 @@ def run_report(self, output_dir: str = BASE_DIR) -> None: [ "quarto", "convert", - os.path.join(output_dir, f"{self.BASE_DIR}.qmd"), + Path(output_dir) / f"{self.BASE_DIR}.qmd", ], check=True, ) @@ -227,7 +225,7 @@ def _create_yaml_header(self) -> str: VueGen - | Ā© 2025 Multiomics Network Analytics Group (MoNA) + | Copyright 2025 Multiomics Network Analytics Group (MoNA) """, r.ReportType.PDF: """ pdf: @@ -273,7 +271,7 @@ def _create_yaml_header(self) -> str: VueGen - | Ā© 2025 Multiomics Network Analytics Group (MoNA) + | Copyright 2025 Multiomics Network Analytics Group (MoNA) """, r.ReportType.PPTX: """ pptx: @@ -304,7 +302,7 @@ def _create_yaml_header(self) -> str: VueGen - | Ā© 2025 Multiomics Network Analytics Group (MoNA) + | Copyright 2025 Multiomics Network Analytics Group (MoNA) """, } # Create a key based on the report type and format @@ -413,13 +411,9 @@ def _generate_plot_content( # Define plot path if is_report_static: - static_plot_path = os.path.join( - static_dir, f"{plot.title.replace(' ', '_')}.png" - ) + static_plot_path = Path(static_dir) / f"{plot.title.replace(' ', '_')}.png" else: - html_plot_file = os.path.join( - static_dir, f"{plot.title.replace(' ', '_')}.html" - ) + html_plot_file = Path(static_dir) / f"{plot.title.replace(' ', '_')}.html" # Add content for the different plot types try: @@ -431,7 +425,7 @@ def _generate_plot_content( plot_content.append(self._generate_plot_code(plot)) if is_report_static: plot_content.append( - f"""fig_plotly.write_image("{os.path.abspath(static_plot_path)}")\n```\n""" + f"""fig_plotly.write_image("{static_plot_path.resolve()}")\n```\n""" ) plot_content.append(self._generate_image_content(static_plot_path)) else: @@ -440,7 +434,7 @@ def _generate_plot_content( plot_content.append(self._generate_plot_code(plot)) if is_report_static: plot_content.append( - f"""fig_altair.save("{os.path.abspath(static_plot_path)}")\n```\n""" + f"""fig_altair.save("{static_plot_path.resolve()}")\n```\n""" ) plot_content.append(self._generate_image_content(static_plot_path)) else: @@ -513,7 +507,7 @@ def _generate_plot_code(self, plot, output_file="") -> str: plot_json = response.text\n""" else: # If it's a local file plot_code += f""" -with open('{os.path.join("..", plot.file_path)}', 'r') as plot_file: +with open('{Path("..") / plot.file_path}', 'r') as plot_file: plot_json = plot_file.read()\n""" # Add specific code for each visualization tool if plot.plot_type == r.PlotType.PLOTLY: @@ -527,7 +521,7 @@ def _generate_plot_code(self, plot, output_file="") -> str: if is_url(plot.file_path) and plot.file_path.endswith(".html"): iframe_src = output_file else: - iframe_src = os.path.join("..", output_file) + iframe_src = Path("..") / output_file # Embed the HTML file in an iframe plot_code = f""" @@ -573,7 +567,7 @@ def _generate_dataframe_content(self, dataframe, is_report_static) -> List[str]: } try: # Check if the file extension matches any DataFrameFormat value - file_extension = os.path.splitext(dataframe.file_path)[1].lower() + file_extension = Path(dataframe.file_path).suffix.lower() if not any( file_extension == fmt.value_with_dot for fmt in r.DataFrameFormat ): @@ -585,7 +579,7 @@ def _generate_dataframe_content(self, dataframe, is_report_static) -> List[str]: file_path = ( dataframe.file_path if is_url(dataframe.file_path) - else os.path.join("..", dataframe.file_path) + else Path("..") / dataframe.file_path ) # Load the DataFrame using the correct function @@ -648,7 +642,7 @@ def _generate_markdown_content(self, markdown) -> List[str]: else: # If it's a local file markdown_content.append( f""" -with open('{os.path.join("..", markdown.file_path)}', 'r') as markdown_file: +with open('{Path("..") / markdown.file_path}', 'r') as markdown_file: markdown_content = markdown_file.read()\n""" ) @@ -694,7 +688,7 @@ def _generate_html_content(self, html) -> List[str]: iframe_src = ( html.file_path if is_url(html.file_path) - else os.path.join("..", html.file_path) + else Path("..") / html.file_path ) iframe_code = f"""
@@ -769,11 +763,9 @@ def _show_dataframe( dataframe_content = [] if is_report_static: # Generate path for the DataFrame image - df_image = os.path.join( - static_dir, f"{dataframe.title.replace(' ', '_')}.png" - ) + df_image = Path(static_dir) / f"{dataframe.title.replace(' ', '_')}.png" dataframe_content.append( - f"df.dfi.export('{os.path.abspath(df_image)}', max_rows=10, max_cols=5)\n```\n" + f"df.dfi.export('{Path(df_image).resolve()}', max_rows=10, max_cols=5)\n```\n" ) # Use helper method to add centered image content dataframe_content.append(self._generate_image_content(df_image)) diff --git a/src/vuegen/report.py b/src/vuegen/report.py index 8564a1d..be2315d 100644 --- a/src/vuegen/report.py +++ b/src/vuegen/report.py @@ -382,7 +382,7 @@ def create_and_save_pyvis_network(self, G: nx.Graph, output_file: str) -> Networ net.show_buttons(filter_=["physics"]) # Save the network as an HTML file - net.save_graph(output_file) + net.save_graph(str(output_file)) self.logger.info(f"PyVis network created and saved as: {output_file}.") return net diff --git a/src/vuegen/streamlit_reportview.py b/src/vuegen/streamlit_reportview.py index 85d9a62..1a9bb76 100644 --- a/src/vuegen/streamlit_reportview.py +++ b/src/vuegen/streamlit_reportview.py @@ -1,5 +1,6 @@ import os import subprocess +from pathlib import Path from typing import List import pandas as pd @@ -14,8 +15,8 @@ class StreamlitReportView(r.WebAppReportView): """ BASE_DIR = "streamlit_report" - SECTIONS_DIR = os.path.join(BASE_DIR, "sections") - STATIC_FILES_DIR = os.path.join(BASE_DIR, "static") + SECTIONS_DIR = Path(BASE_DIR) / "sections" + STATIC_FILES_DIR = Path(BASE_DIR) / "static" REPORT_MANAG_SCRIPT = "report_manager.py" def __init__( @@ -93,7 +94,7 @@ def generate_report( # Create a folder for each section subsection_page_vars = [] section_name_var = section.title.replace(" ", "_") - section_dir_path = os.path.join(output_dir, section_name_var) + section_dir_path = Path(output_dir) / section_name_var if create_folder(section_dir_path): self.report.logger.debug( @@ -106,9 +107,9 @@ def generate_report( for subsection in section.subsections: subsection_name_var = subsection.title.replace(" ", "_") - subsection_file_path = os.path.join( - section_name_var, subsection_name_var + ".py" - ) + subsection_file_path = ( + Path(section_name_var) / f"{subsection_name_var}.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( @@ -128,9 +129,7 @@ def generate_report( ) # Write the navigation and general content to a Python file - with open( - os.path.join(output_dir, self.REPORT_MANAG_SCRIPT), "w" - ) as nav_manager: + with open(Path(output_dir) / self.REPORT_MANAG_SCRIPT, "w") as nav_manager: nav_manager.write("\n".join(report_manag_content)) self.report.logger.info( f"Created app navigation script: {self.REPORT_MANAG_SCRIPT}" @@ -162,7 +161,7 @@ def run_report(self, output_dir: str = SECTIONS_DIR) -> None: [ "streamlit", "run", - os.path.join(output_dir, self.REPORT_MANAG_SCRIPT), + Path(output_dir) / self.REPORT_MANAG_SCRIPT, ], check=True, ) @@ -180,12 +179,12 @@ def run_report(self, output_dir: str = SECTIONS_DIR) -> None: f"To run the Streamlit app, use the following command:" ) self.report.logger.info( - f"streamlit run {os.path.join(output_dir, self.REPORT_MANAG_SCRIPT)}" + f"streamlit run {Path(output_dir) / self.REPORT_MANAG_SCRIPT}" ) msg = ( f"\nAll the scripts to build the Streamlit app are available at: {output_dir}\n\n" f"To run the Streamlit app, use the following command:\n\n" - f"\tstreamlit run {os.path.join(output_dir, self.REPORT_MANAG_SCRIPT)}" + f"\tstreamlit run {Path(output_dir) / self.REPORT_MANAG_SCRIPT}" ) print(msg) @@ -242,7 +241,7 @@ def _generate_home_section( try: # Create folder for the home page - home_dir_path = os.path.join(output_dir, "Home") + home_dir_path = Path(output_dir) / "Home" if create_folder(home_dir_path): self.report.logger.debug(f"Created home directory: {home_dir_path}") else: @@ -267,14 +266,14 @@ def _generate_home_section( home_content.append("st.markdown(footer, unsafe_allow_html=True)\n") # Write the home page content to a Python file - home_page_path = os.path.join(home_dir_path, "Homepage.py") + home_page_path = Path(home_dir_path) / "Homepage.py" with open(home_page_path, "w") as home_page: home_page.write("\n".join(home_content)) self.report.logger.info(f"Home page content written to '{home_page_path}'.") # Add the home page to the report manager content report_manag_content.append( - f"homepage = st.Page('Home/Homepage.py', title='Homepage')" + f"homepage = st.Page('Home/Homepage.py', title='Homepage')" # ! here Posix Path is hardcoded ) report_manag_content.append(f"sections_pages['Home'] = [homepage]\n") self.report.logger.info("Home page added to the report manager content.") @@ -308,10 +307,10 @@ def _generate_sections(self, output_dir: str) -> None: ) try: # Create subsection file - subsection_file_path = os.path.join( - output_dir, - section_name_var, - subsection.title.replace(" ", "_") + ".py", + subsection_file_path = ( + Path(output_dir) + / section_name_var + / f"{subsection.title.replace(' ', '_')}.py" ) # Generate content and imports for the subsection @@ -460,8 +459,8 @@ def _generate_plot_content( networkx_graph, html_plot_file = networkx_graph else: # Otherwise, create and save a new pyvis network from the netowrkx graph - html_plot_file = os.path.join( - static_dir, f"{plot.title.replace(' ', '_')}.html" + html_plot_file = ( + Path(static_dir) / f"{plot.title.replace(' ', '_')}.html" ) pyvis_graph = plot.create_and_save_pyvis_network( networkx_graph, html_plot_file @@ -531,7 +530,7 @@ def _generate_plot_code(self, plot) -> str: plot_json = json.loads(response.text)\n""" else: # If it's a local file plot_code = f""" -with open('{os.path.join(plot.file_path)}', 'r') as plot_file: +with open('{Path(plot.file_path)}', 'r') as plot_file: plot_json = json.load(plot_file)\n""" # Add specific code for each visualization tool @@ -584,7 +583,7 @@ def _generate_dataframe_content(self, dataframe) -> List[str]: try: # Check if the file extension matches any DataFrameFormat value - file_extension = os.path.splitext(dataframe.file_path)[1].lower() + file_extension = Path(dataframe.file_path).suffix.lower() if not any( file_extension == fmt.value_with_dot for fmt in r.DataFrameFormat ): @@ -674,7 +673,7 @@ def _generate_markdown_content(self, markdown) -> List[str]: else: # If it's a local file markdown_content.append( f""" -with open('{os.path.join("..", markdown.file_path)}', 'r') as markdown_file: +with open('{Path("..") / markdown.file_path}', 'r') as markdown_file: markdown_content = markdown_file.read()\n""" ) # Code to display md content @@ -734,7 +733,7 @@ def _generate_html_content(self, html) -> List[str]: # If it's a local file html_content.append( f""" -with open('{os.path.join("..", html.file_path)}', 'r', encoding='utf-8') as html_file: +with open('{Path("..") / html.file_path}', 'r', encoding='utf-8') as html_file: html_content = html_file.read()\n""" ) diff --git a/src/vuegen/utils.py b/src/vuegen/utils.py index e407d34..41fa4e7 100644 --- a/src/vuegen/utils.py +++ b/src/vuegen/utils.py @@ -24,27 +24,20 @@ ## CHECKS -def check_path(filepath: str) -> bool: +def check_path(filepath: Path) -> bool: """ Checks if the given file or folder path exists. Parameters --------- - filepath : str + filepath : Path The file or folder path to check. Returns ------- bool True if the path exists, False otherwise. - - Raises - ------ - AssertionError - If the filepath is not a valid string. """ - # Assert that the filepath is a string - assert isinstance(filepath, str), f"Filepath must be a string: {filepath}" # Check if the path exists return os.path.exists(os.path.abspath(filepath)) @@ -87,13 +80,13 @@ def assert_enum_value( ) -def is_url(filepath: str) -> bool: +def is_url(filepath: Path) -> bool: """ Check if the provided path is a valid URL. Parameters ---------- - filepath : str + filepath : Path The filepath to check. Returns @@ -102,17 +95,9 @@ def is_url(filepath: str) -> bool: True if the input path is a valid URL, meaning it contains both a scheme (e.g., http, https, ftp) and a network location (e.g., example.com). Returns False if either the scheme or the network location is missing or invalid. - - Raises - ------ - AssertionError - If the filepath is not a valid string. """ - # Assert that the filepath is a string - assert isinstance(filepath, str), f"Filepath must be a string: {filepath}" - # Parse the url and return validation - parsed_url = urlparse(filepath) + parsed_url = urlparse(str(filepath)) return bool(parsed_url.scheme and parsed_url.netloc) @@ -749,22 +734,20 @@ def print_completion_message(report_type: str): Prints a formatted completion message after report generation. """ border = "─" * 65 # Creates a separator line - print(f"\n{border}\nšŸŽ‰ Pipeline Execution Complete! šŸŽ‰\n") - if report_type == "streamlit": print( """šŸš€ Streamlit Report Generated! šŸ“‚ All scripts to build the Streamlit app are available at: - nf_container_results/streamlit_report/sections + streamlit_report/sections ā–¶ļø To run the Streamlit app, use the following command: - streamlit run nf_container_results/streamlit_report/sections/report_manager.py + streamlit run streamlit_report/sections/report_manager.py ✨ You can extend the report by adding new files to the input directory or updating the config file. šŸ› ļø Advanced users can modify the Python scripts directly in: - nf_container_results/streamlit_report/sections + streamlit_report/sections """ ) else: @@ -772,12 +755,12 @@ def print_completion_message(report_type: str): f"""šŸš€ {report_type.capitalize()} Report Generated! šŸ“‚ Your {report_type} report is available at: - nf_container_results/quarto_report + quarto_report ✨ You can extend the report by adding new files to the input directory or updating the config file. šŸ› ļø Advanced users can modify the report template directly in: - nf_container_results/quarto_report/quarto_report.qmd + quarto_report/quarto_report.qmd """ ) @@ -810,7 +793,7 @@ def generate_footer() -> str: VueGen - | Ā© 2025 + | Copyright 2025 Multiomics Network Analytics Group (MoNA) """