diff --git a/src/vuegen/quarto_reportview.py b/src/vuegen/quarto_reportview.py index 5a182ce..c4be7a7 100644 --- a/src/vuegen/quarto_reportview.py +++ b/src/vuegen/quarto_reportview.py @@ -223,12 +223,10 @@ def generate_report(self, output_dir: Optional[Path] = None) -> None: fname_qmd_report = self.output_dir / f"{self.BASE_DIR}.qmd" with open(fname_qmd_report, "w", encoding="utf-8") as quarto_report: quarto_report.write(yaml_header) - quarto_report.write( - f"""\n```{{python}} + quarto_report.write(f"""\n```{{python}} #| label: 'Imports' {report_formatted_imports} -```\n\n""" - ) +```\n\n""") quarto_report.write("\n".join(qmd_content)) self.report.logger.info( "Created qmd script to render the app: %s", fname_qmd_report @@ -329,8 +327,7 @@ def _create_yaml_header(self) -> str: A formatted YAML header string customized for the specified output format. """ # Base YAML header with title - yaml_header = textwrap.dedent( - f"""\ + yaml_header = textwrap.dedent(f"""\ --- title: {self.report.title} fig-align: center @@ -338,13 +335,11 @@ def _create_yaml_header(self) -> str: echo: false output: asis jupyter: python3 - format:""" - ) + format:""") # Define format-specific YAML configurations # \u007b is { and \u007d is } format_configs = { - r.ReportType.HTML: textwrap.dedent( - f""" + r.ReportType.HTML: textwrap.dedent(f""" html: toc: true toc-location: left @@ -371,12 +366,10 @@ def _create_yaml_header(self) -> str: | Copyright 2025 {ORG} - """ - ), + """), # \u007b is { and \u007d is } r.ReportType.PDF: textwrap.indent( - textwrap.dedent( - f""" + textwrap.dedent(f""" pdf: toc: false fig-align: center @@ -390,28 +383,22 @@ def _create_yaml_header(self) -> str: \\lofoot\u007bThis report was generated with \\href{{{REPO_URL}}}{{VueGen}} | \\copyright{{}} 2025 \\href{GITHUB_ORG_URL_BRACKETS}\u007b{ORG}\u007d\u007d - \\rofoot{{\\pagemark}}""" - ), + \\rofoot{{\\pagemark}}"""), " ", ), r.ReportType.DOCX: textwrap.indent( - textwrap.dedent( - """ + textwrap.dedent(""" docx: - toc: false""" - ), + toc: false"""), " ", ), r.ReportType.ODT: textwrap.indent( - textwrap.dedent( - """ + textwrap.dedent(""" odt: - toc: false""" - ), + toc: false"""), " ", ), - r.ReportType.REVEALJS: textwrap.dedent( - f""" + r.ReportType.REVEALJS: textwrap.dedent(f""" revealjs: toc: false smaller: true @@ -439,19 +426,15 @@ def _create_yaml_header(self) -> str: | Copyright 2025 {ORG} - """ - ), + """), r.ReportType.PPTX: textwrap.indent( - textwrap.dedent( - """ + textwrap.dedent(""" pptx: toc: false - output: true""" - ), + output: true"""), " ", ), - r.ReportType.JUPYTER: textwrap.dedent( - f""" + r.ReportType.JUPYTER: textwrap.dedent(f""" html: toc: true toc-location: left @@ -478,8 +461,7 @@ def _create_yaml_header(self) -> str: | Copyright 2025 {ORG} - """ - ), + """), } # Create a key based on the report type and format key = self.report_type @@ -692,36 +674,29 @@ def _generate_plot_code(self, plot, output_file="") -> str: The generated plot code as a string. """ # Initialize plot code with common structure - plot_code = textwrap.dedent( - f""" + plot_code = textwrap.dedent(f""" ```{{python}} #| label: '{plot.title} {plot.id}' #| fig-cap: "" - """ - ) + """) # If the file path is a URL, generate code to fetch content via requests if is_url(plot.file_path): - plot_code += textwrap.dedent( - f""" + plot_code += textwrap.dedent(f""" response = requests.get('{plot.file_path}') response.raise_for_status() plot_json = response.text - """ - ) + """) else: # If it's a local file plot_rel_path = get_relative_file_path( plot.file_path, relative_to=self.output_dir ).as_posix() - plot_code += textwrap.dedent( - f""" + plot_code += textwrap.dedent(f""" with open(report_dir /'{plot_rel_path}', 'r') as plot_file: plot_json = json.load(plot_file) -""" - ) +""") # Add specific code for each visualization tool if plot.plot_type == r.PlotType.PLOTLY: - plot_code += textwrap.dedent( - """ + plot_code += textwrap.dedent(""" # Keep only 'data' and 'layout' sections plot_json = {key: plot_json[key] for key in plot_json if key in ['data', 'layout'] @@ -737,19 +712,16 @@ def _generate_plot_code(self, plot, output_file="") -> str: fig_plotly.update_layout(autosize=False, width=950, height=400, margin=dict(b=50, t=50, l=50, r=50) ) - """ - ) + """) elif plot.plot_type == r.PlotType.ALTAIR: - plot_code += textwrap.dedent( - """ + plot_code += textwrap.dedent(""" # Convert JSON to string plot_json_str = json.dumps(plot_json) # Create the altair plot fig_altair = alt.Chart.from_json(plot_json_str ).properties(width=900, height=370) - """ - ) + """) elif plot.plot_type == r.PlotType.INTERACTIVE_NETWORK: # Generate the HTML embedding for interactive networks if is_url(plot.file_path) and plot.file_path.endswith(".html"): @@ -760,15 +732,13 @@ def _generate_plot_code(self, plot, output_file="") -> str: ) # Embed the HTML file in an iframe - plot_code = textwrap.dedent( - f""" + plot_code = textwrap.dedent(f"""
- """ - ) + """) return plot_code def _generate_dataframe_content(self, dataframe) -> List[str]: @@ -790,15 +760,11 @@ def _generate_dataframe_content(self, dataframe) -> List[str]: dataframe_content.append(f"### {dataframe.title}") # Append header for DataFrame loading - dataframe_content.append( - textwrap.dedent( - f"""\ + dataframe_content.append(textwrap.dedent(f"""\ ```{{python}} #| label: '{dataframe.title} {dataframe.id}' #| fig-cap: "" - """ - ) - ) + """)) # Mapping of file extensions to read functions read_function_mapping = table_utils.read_function_mapping try: @@ -856,15 +822,11 @@ def _generate_dataframe_content(self, dataframe) -> List[str]: if sheet_names: for sheet_name in sheet_names[1:]: dataframe_content.append(f"#### {sheet_name}") - dataframe_content.append( - textwrap.dedent( - f"""\ + dataframe_content.append(textwrap.dedent(f"""\ ```{{python}} #| label: '{dataframe.title} {dataframe.id} {sheet_name}' #| fig-cap: "" - """ - ) - ) + """)) dataframe_content.append( f"df = pd.{read_function.__name__}" f"(report_dir / '{df_file_path}', " @@ -913,35 +875,25 @@ def _generate_markdown_content(self, markdown) -> List[str]: try: # Initialize md code with common structure - markdown_content.append( - textwrap.dedent( - f""" + markdown_content.append(textwrap.dedent(f""" ```{{python}} #| label: '{markdown.title} {markdown.id}' #| fig-cap: "" - """ - ) - ) + """)) # If the file path is a URL, generate code to fetch content via requests if is_url(markdown.file_path): - markdown_content.append( - textwrap.dedent( - f"""\ + markdown_content.append(textwrap.dedent(f"""\ response = requests.get('{markdown.file_path}') response.raise_for_status() markdown_content = response.text - """ - ) - ) + """)) else: # If it's a local file md_rel_path = get_relative_file_path( markdown.file_path, relative_to=self.output_dir ) - markdown_content.append( - f""" + markdown_content.append(f""" with open(report_dir / '{md_rel_path.as_posix()}', 'r') as markdown_file: - markdown_content = markdown_file.read()\n""" - ) + markdown_content = markdown_file.read()\n""") # Code to display md content markdown_content.append("""display.Markdown(markdown_content)\n```\n""") @@ -1037,14 +989,12 @@ def _generate_html_content(self, html) -> List[str]: html_file_path = get_relative_file_path( html.file_path, relative_to=self.output_dir ) - iframe_code = textwrap.dedent( - f""" + iframe_code = textwrap.dedent(f"""
- """ - ) + """) html_content.append(iframe_code) except Exception as e: diff --git a/src/vuegen/streamlit_reportview.py b/src/vuegen/streamlit_reportview.py index cac5b30..00e8113 100644 --- a/src/vuegen/streamlit_reportview.py +++ b/src/vuegen/streamlit_reportview.py @@ -131,38 +131,26 @@ def generate_report(self, output_dir: str = None) -> None: self.report.logger.debug("Processing app navigation code.") # Define the Streamlit imports and report manager content report_manag_content = [] - report_manag_content.append( - textwrap.dedent( - """\ + report_manag_content.append(textwrap.dedent("""\ import os import time import psutil import streamlit as st - """ - ) - ) + """)) if self.report.logo: - report_manag_content.append( - textwrap.dedent( - f"""\ + report_manag_content.append(textwrap.dedent(f"""\ st.set_page_config(layout="wide", page_title="{self.report.title}", page_icon="{self.report.logo}" ) st.logo("{self.report.logo}") - """ - ) - ) + """)) else: - report_manag_content.append( - textwrap.dedent( - f"""\ + report_manag_content.append(textwrap.dedent(f"""\ st.set_page_config(layout="wide", page_title="{self.report.title}") - """ - ) - ) + """)) report_manag_content.append( self._format_text( text=self.report.title, type="header", level=1, color="#023858" @@ -245,9 +233,7 @@ def generate_report(self, output_dir: str = None) -> None: ) # Add navigation object to the home page content - report_manag_content.append( - textwrap.dedent( - """\ + report_manag_content.append(textwrap.dedent("""\ report_nav = st.navigation(sections_pages) # Following https://discuss.streamlit.io/t/\ @@ -265,9 +251,7 @@ def generate_report(self, output_dir: str = None) -> None: report_nav.run() - """ - ) - ) + """)) # Write the navigation and general content to a Python file with open( @@ -285,9 +269,7 @@ def generate_report(self, output_dir: str = None) -> None: fpath = self.section_dir.parent / "README.md" with open(fpath, "w", encoding="utf-8") as f: - f.write( - textwrap.dedent( - f"""\ + f.write(textwrap.dedent(f"""\ # Streamlit Report This report was generated using the Vuegen library: @@ -299,9 +281,7 @@ def generate_report(self, output_dir: str = None) -> None: Folder cannot be moved from above path, but can be executed from anywhere on the system. - """ - ) - ) + """)) except Exception as e: self.report.logger.error( @@ -425,8 +405,7 @@ def _format_text( text = text.strip() # get rid of new lines text = textwrap.indent(text, " ") - ret = textwrap.dedent( - f"""\ + ret = textwrap.dedent(f"""\ st.markdown( ''' <{tag} style='text-align: {text_align}; @@ -434,8 +413,7 @@ def _format_text( ''', unsafe_allow_html=True) - """ - ) + """) return ret def _generate_home_section( @@ -730,33 +708,23 @@ def _generate_plot_content(self, plot) -> List[str]: # Determine whether the file path is a URL or a local file if is_url(html_plot_file): - plot_content.append( - textwrap.dedent( - f""" + plot_content.append(textwrap.dedent(f""" response = requests.get('{html_plot_file}') response.raise_for_status() html_content = response.text - """ - ) - ) + """)) else: fpath = get_relative_file_path( html_plot_file, relative_to=self.section_dir ).as_posix() - plot_content.append( - textwrap.dedent( - f""" + plot_content.append(textwrap.dedent(f""" file_path = (section_dir / '{fpath}').resolve().as_posix() with open(file_path, 'r') as html_file: html_content = html_file.read() - """ - ) - ) + """)) # Append the code for additional information (nodes and edges count) - plot_content.append( - textwrap.dedent( - f""" + plot_content.append(textwrap.dedent(f""" st.markdown(("

" "Number of nodes: {num_nodes}

"), unsafe_allow_html=True) @@ -764,9 +732,7 @@ def _generate_plot_content(self, plot) -> List[str]: " Number of relationships: {num_edges}" "

"), unsafe_allow_html=True) - """ - ) - ) + """)) # Add the specific code for visualization plot_content.append(self._generate_plot_code(plot)) @@ -807,27 +773,22 @@ def _generate_plot_code(self, plot) -> str: """ # If the file path is a URL, generate code to fetch content via requests if is_url(plot.file_path): - plot_code = textwrap.dedent( - f""" + plot_code = textwrap.dedent(f""" response = requests.get('{plot.file_path}') response.raise_for_status() - plot_json = json.loads(response.text)\n""" - ) + plot_json = json.loads(response.text)\n""") else: # If it's a local file plot_rel_path = get_relative_file_path( plot.file_path, relative_to=self.section_dir ).as_posix() - plot_code = textwrap.dedent( - f""" + plot_code = textwrap.dedent(f""" file_path = (section_dir / '{plot_rel_path}').resolve().as_posix() with open(file_path, 'r') as plot_file: - plot_json = json.load(plot_file)\n""" - ) + plot_json = json.load(plot_file)\n""") # Add specific code for each visualization tool if plot.plot_type == r.PlotType.PLOTLY: - plot_code += textwrap.dedent( - """ + plot_code += textwrap.dedent(""" # Keep only 'data' and 'layout' sections plot_json = {key: plot_json[key] for key in plot_json if key in ['data', 'layout']} @@ -835,26 +796,21 @@ def _generate_plot_code(self, plot) -> str: # Remove 'frame' section in 'data' plot_json['data'] = [{k: v for k, v in entry.items() if k != 'frame'} for entry in plot_json.get('data', [])] - st.plotly_chart(plot_json, use_container_width=True)\n""" - ) + st.plotly_chart(plot_json, use_container_width=True)\n""") elif plot.plot_type == r.PlotType.ALTAIR: - plot_code += textwrap.dedent( - """ + plot_code += textwrap.dedent(""" altair_plot = alt.Chart.from_dict(plot_json) st.vega_lite_chart(json.loads(altair_plot.to_json()), - use_container_width=True)\n""" - ) + use_container_width=True)\n""") elif plot.plot_type == r.PlotType.INTERACTIVE_NETWORK: - plot_code = textwrap.dedent( - """\ + plot_code = textwrap.dedent("""\ # Streamlit checkbox for controlling the layout control_layout = st.checkbox('Add panel to control layout', value=True) net_html_height = 1200 if control_layout else 630 # Load HTML into HTML component for display on Streamlit - st.components.v1.html(html_content, height=net_html_height)\n""" - ) + st.components.v1.html(html_content, height=net_html_height)\n""") return plot_code def _generate_dataframe_content(self, dataframe) -> List[str]: @@ -914,17 +870,13 @@ def _generate_dataframe_content(self, dataframe) -> List[str]: fpath = get_relative_file_path( dataframe.file_path, relative_to=self.section_dir ).as_posix() - dataframe_content.append( - textwrap.dedent( - f"""\ + dataframe_content.append(textwrap.dedent(f"""\ file_path = (section_dir / '{fpath}').resolve().as_posix() sheet_names = table_utils.get_sheet_names(file_path) selected_sheet = st.selectbox("Select a sheet to display", options=sheet_names, ) - """ - ) - ) + """)) # Load the DataFrame using the correct function df_file_path = get_relative_file_path( @@ -935,14 +887,10 @@ def _generate_dataframe_content(self, dataframe) -> List[str]: r.DataFrameFormat.XLS.value_with_dot, r.DataFrameFormat.XLSX.value_with_dot, ]: - dataframe_content.append( - textwrap.dedent( - f"""\ + dataframe_content.append(textwrap.dedent(f"""\ file_path = (section_dir / '{df_file_path}').resolve() df = pd.{read_function}(file_path, sheet_name=selected_sheet) - """ - ) - ) + """)) else: dataframe_content.append( f"file_path = (section_dir / '{df_file_path}'" @@ -951,9 +899,7 @@ def _generate_dataframe_content(self, dataframe) -> List[str]: ) # ! Alternative to select box: iterate over sheets in DataFrame # Displays a DataFrame using AgGrid with configurable options. - dataframe_content.append( - textwrap.dedent( - """ + dataframe_content.append(textwrap.dedent(""" # Displays a DataFrame using AgGrid with configurable options. grid_builder = GridOptionsBuilder.from_dataframe(df) grid_builder.configure_default_column(editable=True, @@ -980,9 +926,7 @@ def _generate_dataframe_content(self, dataframe) -> List[str]: file_name=f"dataframe_{df_index}.csv", mime='text/csv', key=f"download_button_{df_index}") - df_index += 1""" - ) - ) + df_index += 1""")) except Exception as e: self.report.logger.error( "Error generating content for DataFrame: %s. Error: %s", @@ -1031,29 +975,21 @@ def _generate_markdown_content(self, markdown) -> List[str]: try: # If the file path is a URL, generate code to fetch content via requests if is_url(markdown.file_path): - markdown_content.append( - textwrap.dedent( - f""" + markdown_content.append(textwrap.dedent(f""" response = requests.get('{markdown.file_path}') response.raise_for_status() markdown_content = response.text - """ - ) - ) + """)) else: # If it's a local file md_rel_path = get_relative_file_path( markdown.file_path, relative_to=self.section_dir ).as_posix() - markdown_content.append( - textwrap.dedent( - f""" + markdown_content.append(textwrap.dedent(f""" file_path = (section_dir / '{md_rel_path}').resolve().as_posix() with open(file_path, 'r') as markdown_file: markdown_content = markdown_file.read() - """ - ) - ) + """)) # Code to display md content markdown_content.append( "st.markdown(markdown_content, unsafe_allow_html=True)\n" @@ -1105,28 +1041,20 @@ def _generate_html_content(self, html) -> List[str]: try: if is_url(html.file_path): # If it's a URL, fetch content dynamically - textwrap.dedent( - html_content.append( - f""" + textwrap.dedent(html_content.append(f""" response = requests.get('{html.file_path}') response.raise_for_status() html_content = response.text - """ - ) - ) + """)) else: # If it's a local file html_rel_path = get_relative_file_path( html.file_path, relative_to=self.section_dir ).as_posix() - html_content.append( - textwrap.dedent( - f"""\ + html_content.append(textwrap.dedent(f"""\ file_path = (section_dir / '{html_rel_path}').resolve().as_posix() with open(file_path, 'r', encoding='utf-8') as f: html_content = f.read() - """ - ) - ) + """)) # Display HTML content using Streamlit html_content.append( @@ -1284,8 +1212,7 @@ def _generate_chatbot_content(self, chatbot) -> List[str]: if chatbot.model: # --- Ollama-style streaming chatbot --- # all other codeblocks pasted in need to be on this indentation level - code_block = textwrap.dedent( - f""" + code_block = textwrap.dedent(f""" {init_messages_block} # Function to send prompt to Ollama API def generate_query(messages): @@ -1334,14 +1261,12 @@ def response_generator(msg_content): st.session_state.messages.append(parsed_response) with st.chat_message("assistant"): st.write_stream(response_generator(parsed_response["content"])) - """ - ) + """) chatbot_content.append(code_block) else: # --- Standard (non-streaming) API chatbot --- - code_block = textwrap.dedent( - f""" + code_block = textwrap.dedent(f""" {init_messages_block} # Function to send prompt to standard API @@ -1391,8 +1316,7 @@ def generate_query(prompt): ) else: st.error("Failed to get response from API") - """ - ) + """) chatbot_content.append(code_block) if chatbot.caption: diff --git a/src/vuegen/utils/__init__.py b/src/vuegen/utils/__init__.py index 86c638b..78f4634 100644 --- a/src/vuegen/utils/__init__.py +++ b/src/vuegen/utils/__init__.py @@ -835,8 +835,7 @@ def get_completion_message(report_type: str, config_path: str) -> str: border = "─" * 65 # Creates a separator line if report_type == "streamlit": - message = textwrap.dedent( - f""" + message = textwrap.dedent(f""" 🚀 Streamlit Report Generated! 📂 All scripts to build the Streamlit app are available at: @@ -853,11 +852,9 @@ def get_completion_message(report_type: str, config_path: str) -> str: ⚙️ Configuration file used: {config_path} - """ - ) + """) else: - message = textwrap.dedent( - f""" + message = textwrap.dedent(f""" 🚀 {report_type.capitalize()} Report Generated! 📂 Your {report_type} report is available at: @@ -871,8 +868,7 @@ def get_completion_message(report_type: str, config_path: str) -> str: ⚙️ Configuration file used: {config_path} - """ - ) + """) return f"{message}\n{border}" @@ -891,8 +887,7 @@ def generate_footer() -> str: str A formatted HTML string representing the footer. """ - footer = textwrap.dedent( - f""" + footer = textwrap.dedent(f"""