import logging from typing import List, Any, Dict from abc import ABC, abstractmethod import streamlit as st from streamlit.delta_generator import DeltaGenerator from utility_classes.dashboard_config_parser import DashboardConfig from streamlit_constants import DASHBOARD_CONFIG_OBJECT_KEY class Renderable(ABC): """ Abstract base class for UI components drawn to a Streamlit container. Provides a strict contract for rendering and manages unique identifier generation. This structure is critical in Streamlit to prevent widget state collisions when multiple instances of the same UI component are rendered simultaneously. :param instance_id: A unique identifier for this specific rendered instance. :type instance_id: str """ def __init__(self, instance_id: str): self.instance_id = instance_id @abstractmethod def render(self, container: DeltaGenerator): """ Renders the component's UI elements onto the specified Streamlit container. :param container: The Streamlit layout element where this component will be drawn. :type container: DeltaGenerator """ raise NotImplementedError() def get_widget_key(self, widget_unique_id: str) -> str: """ Generates a globally unique Streamlit widget key for this specific instance. :param widget_unique_id: The local identifier for the specific widget. :type widget_unique_id: str :return: A concatenated string combining the instance ID and widget ID. :rtype: str """ return f'{self.instance_id}_{widget_unique_id}' class BaseReportPage(Renderable): """ Standardizes the rendering pipeline for all analytical report pages. This class breaks down UI generation into a predictable, synchronous lifecycle: 1. Render controls (capture inputs). 2. Generate figures (process data). 3. Render figures (display outputs). This strict pipeline allows higher-level orchestrators to intercept the process mid-cycle (e.g., to mutate generated figures before they are drawn). :param title: The title of the page, acting as its base instance ID. :type title: str """ def __init__(self, title:str): # set the instance id to the title of the page super().__init__(title) self.title = title self.logger = logging.getLogger(__name__) self.app_config:DashboardConfig = st.session_state[DASHBOARD_CONFIG_OBJECT_KEY] @staticmethod def get_page_name(): """ Retrieves the human-readable name of the report for UI selection menus. :return: The display name of the report. :rtype: str """ return "" def render_controls(self, container: DeltaGenerator) -> Dict[str, Any]: """ Renders the input widgets required to parameterize the report. :param container: The Streamlit container to draw the widgets onto. :type container: DeltaGenerator :return: A dictionary of user-selected parameters, or None if inputs are invalid. :rtype: Dict[str, Any] """ raise NotImplementedError() def generate_figures(self, parameters: Dict[str, Any]): """ Processes parameters and generates the underlying data/figures for the report. :param parameters: The dictionary of user inputs captured from render_controls. :type parameters: Dict[str, Any] :return: A dictionary of computed outputs or Plotly figures. :rtype: Dict[str, Any] """ raise NotImplementedError() def render_figures(self, container: DeltaGenerator, output_data: Dict[str, Any]): """ Draws the previously generated figures and data onto the screen. :param container: The Streamlit container to draw the visuals onto. :type container: DeltaGenerator :param output_data: The computed results returned by generate_figures. :type output_data: Dict[str, Any] """ raise NotImplementedError() def get_syncable_figure_keys(self) -> List[str]: """ Identifies which generated figures support external axis synchronization for when two of the same report are displayed side by side. This should really only be used for graphs for which axis synchronization makes sense (like bar charts that show quantities not percentages) :return: A list of dictionary keys corresponding to figures in output_data that can have their Y-axes scaled dynamically by a parent orchestrator. :rtype: List[str] """ # Default to no synchronization return [] def render(self, container: DeltaGenerator): """ Executes the standardized report lifecycle. Sequentially chains control rendering, data generation, and figure drawing, passing state safely between each phase and halting if user parameters are missing. :param container: The Streamlit layout container for the entire report. :type container: DeltaGenerator """ parameters = self.render_controls(container) # Only proceed if the user has provided valid inputs if parameters is not None: print("Got parameters") output_data = self.generate_figures(parameters) if output_data is not None: print("got output data") self.render_figures(container, output_data) else: self.logger.error( "No output figures or objects were provided to render this page, if no renderable figures are desired, an empty dictionary should be returned.") st.error( f"No objects to render this page were provided. A detailed error has been added to the logs. {self.app_config.get_errors_contact_string()}") st.stop() else: self.logger.error("No parameters were provided to render this page, if no parameters are desired an empty dictionary should be returned.") st.error(f"No parameters to render this page were provided. A detailed error has been added to the logs. {self.app_config.get_errors_contact_string()}") st.stop()