first commit

This commit is contained in:
2026-05-21 08:40:24 -04:00
commit b084545275
711 changed files with 3659856 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
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()

View File

@@ -0,0 +1,374 @@
import yaml
from dataclasses import dataclass
from typing import Dict
import copy
import logging
@dataclass
class ExportModulePair:
"""
Data structure pairing data export endpoints across adjacent fiscal periods.
This class groups the URLs for the current and previous fiscal years, allowing
the application to easily pass paired historical and active data sources to
downstream report pipelines.
"""
current_fy:str | None = None
prev_fy:str | None = None
# Export module configuration file constants
EXPORT_MODULE_URLS_KEY = "export_module_urls"
EXPORT_MODULE_CLIENT_LIST_KEY = "clients_list"
EXPORT_MODULE_NBS_KEY = "new_business_starts"
EXPORT_MODULE_FUNDING_KEY = "capital_funding"
EXPORT_MODULE_TRAININGS_KEY = "trainings"
CURRENT_FY_KEY = "current_fy"
PREV_FY_KEY = "prev_fy"
LOGGING_CONFIG_SECTION_KEY = "logging"
LOGGING_LEVEL_KEY = "level"
LOGGING_FILE_KEY = "file"
ERRORS_SECTION_KEY = "errors"
CONTACT_NAME_KEY = "contact_name"
CONTACT_METHOD_KEY = "contact_method"
USDA_API_SECTION_KEY = "usda_api"
USDA_API_KEY_KEY = "key"
CENSUS_SECTION_KEY = "census"
CENSUS_YEAR_KEY = "year"
TEMPLATE_DICT = {
EXPORT_MODULE_URLS_KEY: {
EXPORT_MODULE_CLIENT_LIST_KEY: {
CURRENT_FY_KEY: "",
PREV_FY_KEY: ""
},
EXPORT_MODULE_NBS_KEY: {
CURRENT_FY_KEY: "",
PREV_FY_KEY: ""
},
EXPORT_MODULE_FUNDING_KEY: {
CURRENT_FY_KEY: "",
PREV_FY_KEY: ""
},
EXPORT_MODULE_TRAININGS_KEY: {
CURRENT_FY_KEY: "",
PREV_FY_KEY: ""
}
},
LOGGING_CONFIG_SECTION_KEY: {
LOGGING_LEVEL_KEY: "WARNING",
LOGGING_FILE_KEY: "dashboard_log.log"
},
ERRORS_SECTION_KEY: {
CONTACT_NAME_KEY: "",
CONTACT_METHOD_KEY: "",
},
USDA_API_SECTION_KEY: {
USDA_API_KEY_KEY: "",
},
CENSUS_SECTION_KEY: {
CENSUS_YEAR_KEY: "",
}
}
class DashboardConfig:
"""
Central manager for application configuration state and schema validation.
This class handles the serialization, parsing, and strict validation of the YAML
configuration file. It acts as the single source of truth for all external Neoserra
data endpoints and logging behaviors, abstracting the dictionary traversal away from
the reporting classes.
:param filename: The filepath to the target YAML configuration file.
:type filename: str
"""
def __init__(self, filename:str):
"""
Initializes the configuration manager with a default structural template.
Establishes the base dictionary structure in memory before attempting any file I/O
operations, ensuring internal accessors do not fail on missing root keys.
:param filename: The path to the configuration file on disk.
:type filename: str
"""
self.config_dictionary:Dict = copy.deepcopy(TEMPLATE_DICT)
self.filename = filename
def set_clients_list_current_fy_url(self, current_fy_url:str):
"""
Updates the in-memory endpoint URL for the current fiscal period's client dataset.
"""
self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_CLIENT_LIST_KEY][CURRENT_FY_KEY] = current_fy_url
def set_clients_list_prev_fy_url(self, prev_fy_url:str):
"""
Updates the in-memory endpoint URL for the previous fiscal period's client dataset.
"""
self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_CLIENT_LIST_KEY][PREV_FY_KEY] = prev_fy_url
def get_clients_list_urls(self) -> ExportModulePair:
"""
Retrieves the paired endpoints for the client list dataset.
:return: An object containing the current and previous fiscal period URLs.
:rtype: ExportModulePair
"""
return ExportModulePair(
current_fy=self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_CLIENT_LIST_KEY][CURRENT_FY_KEY],
prev_fy=self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_CLIENT_LIST_KEY][PREV_FY_KEY]
)
def set_nbs_milestones_current_fy_url(self, current_fy_url:str):
"""
Updates the in-memory endpoint URL for the current fiscal period's New Business Starts dataset.
"""
self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_NBS_KEY][CURRENT_FY_KEY] = current_fy_url
def set_nbs_milestones_prev_fy_url(self, prev_fy_url:str):
"""
Updates the in-memory endpoint URL for the previous fiscal period's New Business Starts dataset.
"""
self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_NBS_KEY][PREV_FY_KEY] = prev_fy_url
def get_nbs_milestones_urls(self) -> ExportModulePair:
"""
Retrieves the paired endpoints for the New Business Starts dataset.
:return: An object containing the current and previous fiscal period URLs.
:rtype: ExportModulePair
"""
return ExportModulePair(
current_fy=self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_NBS_KEY][CURRENT_FY_KEY],
prev_fy=self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_NBS_KEY][PREV_FY_KEY]
)
def set_funding_milestones_current_fy_url(self, current_fy_url:str):
"""
Updates the in-memory endpoint URL for the current fiscal period's funding milestones dataset.
"""
self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_FUNDING_KEY][CURRENT_FY_KEY] = current_fy_url
def set_funding_milestones_prev_fy_url(self, prev_fy_url:str):
"""
Updates the in-memory endpoint URL for the previous fiscal period's funding milestones dataset.
"""
self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_FUNDING_KEY][PREV_FY_KEY] = prev_fy_url
def get_funding_milestones_urls(self) -> ExportModulePair:
"""
Retrieves the paired endpoints for the funding milestones dataset.
:return: An object containing the current and previous fiscal period URLs.
:rtype: ExportModulePair
"""
return ExportModulePair(
current_fy=self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_FUNDING_KEY][CURRENT_FY_KEY],
prev_fy=self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_FUNDING_KEY][PREV_FY_KEY]
)
def set_trainings_current_fy_url(self, current_fy_url:str):
"""
Updates the in-memory endpoint URL for the current fiscal period's training events dataset.
"""
self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_TRAININGS_KEY][CURRENT_FY_KEY] = current_fy_url
def set_trainings_prev_fy_url(self, prev_fy_url:str):
"""
Updates the in-memory endpoint URL for the previous fiscal period's training events dataset.
"""
self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_TRAININGS_KEY][PREV_FY_KEY] = prev_fy_url
def get_trainings_urls(self) -> ExportModulePair:
"""
Retrieves the paired endpoints for the training events dataset.
:return: An object containing the current and previous fiscal period URLs.
:rtype: ExportModulePair
"""
return ExportModulePair(
current_fy=self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_TRAININGS_KEY][CURRENT_FY_KEY],
prev_fy=self.config_dictionary[EXPORT_MODULE_URLS_KEY][EXPORT_MODULE_TRAININGS_KEY][PREV_FY_KEY]
)
def set_usda_api_key(self, key:str):
self.config_dictionary[USDA_API_SECTION_KEY][USDA_API_KEY_KEY] = key
def get_usda_api_key(self):
return self.config_dictionary[USDA_API_SECTION_KEY][USDA_API_KEY_KEY]
def set_census_year(self, year:str):
self.config_dictionary[CENSUS_SECTION_KEY][CENSUS_YEAR_KEY] = year
def get_census_year(self):
return self.config_dictionary[CENSUS_SECTION_KEY][CENSUS_YEAR_KEY]
def get_log_path(self) -> str | None:
"""
Retrieves the configured file path for the application's rotating file logger.
:return: The target filepath, or None if the configuration is missing.
:rtype: str | None
"""
try:
return self.config_dictionary[LOGGING_CONFIG_SECTION_KEY][LOGGING_FILE_KEY]
except KeyError:
return None
def get_log_level(self) -> int:
"""
Translates the human-readable logging level from the configuration file into system constants.
Maps string values (e.g., "DEBUG", "WARNING") to their corresponding integer values
required by the native Python `logging` module.
:return: The integer constant representing the logging severity level.
:rtype: int
"""
try:
log_level = self.config_dictionary[LOGGING_CONFIG_SECTION_KEY][LOGGING_LEVEL_KEY]
except KeyError:
return None
if log_level == "DEBUG":
return logging.DEBUG
elif log_level == "INFO":
return logging.INFO
elif log_level == "WARNING" or log_level == "WARN":
return logging.WARNING
elif log_level == "ERROR":
return logging.ERROR
elif log_level == "CRITICAL":
return logging.CRITICAL
else:
return None
def set_errors_contact_name(self, name:str):
self.config_dictionary[ERRORS_SECTION_KEY][CONTACT_NAME_KEY] = name
def get_errors_contact_name(self) -> str:
return self.config_dictionary[ERRORS_SECTION_KEY][CONTACT_NAME_KEY]
def set_errors_contact_method(self, method:str):
self.config_dictionary[ERRORS_SECTION_KEY][CONTACT_METHOD_KEY] = method
def get_errors_contact_method(self):
return self.config_dictionary[ERRORS_SECTION_KEY][CONTACT_METHOD_KEY]
def get_errors_contact_string(self) -> str:
return f"Contact {self.get_errors_contact_name()} ({self.get_errors_contact_method()})"
@staticmethod
def write_template(filename:str):
"""
Bootstraps a new environment by generating a default configuration file on disk.
Writes the hardcoded `TEMPLATE_DICT` schema to the specified path to ensure administrators
have the correct structural format when deploying the dashboard for the first time.
:param filename: The target filepath for the generated YAML file.
:type filename: str
"""
with open(filename, "w") as stream:
try:
yaml.dump(TEMPLATE_DICT, stream, default_flow_style=False, indent=4)
except Exception as e:
raise IOError("Could not dump configuration template") from e
def save(self):
"""
Serializes the current in-memory configuration state to the YAML file on disk.
"""
with open(self.filename, "w") as stream:
try:
yaml.dump(self.config_dictionary, stream, default_flow_style=False, indent=4)
except Exception as e:
raise IOError("Could not save configuration file!") from e
def load(self):
"""
Reads the configuration file from disk and enforces strict schema validation.
Acts as a fail-fast gateway for the application environment. It manually verifies the
existence of every required organizational key (export modules, fiscal periods, and logging
parameters). If the YAML file is structurally malformed or missing endpoints, it immediately
raises explicit KeyErrors to halt application boot, preventing cascading fetch failures later.
"""
try:
with open(self.filename, "r") as stream:
config = yaml.safe_load(stream)
self.config_dictionary = config
export_module_urls = config.get(EXPORT_MODULE_URLS_KEY, None)
# Parse all of the export module urls form the config file
if export_module_urls is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have config section {EXPORT_MODULE_URLS_KEY}.")
# Handle the clients list section
clients_section = export_module_urls.get(EXPORT_MODULE_CLIENT_LIST_KEY, None)
if clients_section is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have config section {EXPORT_MODULE_CLIENT_LIST_KEY} under section {EXPORT_MODULE_URLS_KEY}.")
clients_section_current = clients_section.get(CURRENT_FY_KEY, None)
clients_section_prev = clients_section.get(PREV_FY_KEY, None)
if clients_section_current is None or clients_section_prev is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have either a current fiscal year or a previous fiscal year section underneath section {EXPORT_MODULE_URLS_KEY}/{EXPORT_MODULE_CLIENT_LIST_KEY}. Add '{CURRENT_FY_KEY}:' and '{PREV_FY_KEY}:' under this config section.")
nbs_section = export_module_urls.get(EXPORT_MODULE_NBS_KEY, None)
if nbs_section is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have config section {EXPORT_MODULE_NBS_KEY} under section {EXPORT_MODULE_URLS_KEY}.")
nbs_section_current = nbs_section.get(CURRENT_FY_KEY, None)
nbs_section_prev = nbs_section.get(PREV_FY_KEY, None)
if nbs_section_current is None or nbs_section_prev is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have either a current fiscal year or a previous fiscal year section underneath section {EXPORT_MODULE_URLS_KEY}/{EXPORT_MODULE_NBS_KEY}. Add '{CURRENT_FY_KEY}:' and '{PREV_FY_KEY}:' under this config section.")
funding_section = export_module_urls.get(EXPORT_MODULE_FUNDING_KEY, None)
if funding_section is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have config section {EXPORT_MODULE_FUNDING_KEY} under section {EXPORT_MODULE_URLS_KEY}.")
funding_section_current = funding_section.get(CURRENT_FY_KEY, None)
funding_section_prev = funding_section.get(PREV_FY_KEY, None)
if funding_section_current is None or funding_section_prev is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have either a current fiscal year or a previous fiscal year section underneath section {EXPORT_MODULE_URLS_KEY}/{EXPORT_MODULE_FUNDING_KEY}. Add '{CURRENT_FY_KEY}:' and '{PREV_FY_KEY}:' under this config section.")
trainings_section = export_module_urls.get(EXPORT_MODULE_TRAININGS_KEY, None)
if trainings_section is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have config section {EXPORT_MODULE_TRAININGS_KEY} under section {EXPORT_MODULE_URLS_KEY}.")
trainings_section_current = trainings_section.get(CURRENT_FY_KEY, None)
trainings_section_prev = trainings_section.get(PREV_FY_KEY, None)
if trainings_section_current is None or trainings_section_prev is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have either a current fiscal year or a previous fiscal year section underneath section {EXPORT_MODULE_URLS_KEY}/{EXPORT_MODULE_TRAININGS_KEY}. Add '{CURRENT_FY_KEY}:' and '{PREV_FY_KEY}:' under this config section.")
logging_section = self.config_dictionary.get(LOGGING_CONFIG_SECTION_KEY, None)
if logging_section is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have a config section {LOGGING_CONFIG_SECTION_KEY}.")
logging_section_level = logging_section.get(LOGGING_LEVEL_KEY, None)
if logging_section_level is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have a config section {LOGGING_LEVEL_KEY} under section {LOGGING_CONFIG_SECTION_KEY}.")
logging_section_file = logging_section.get(LOGGING_FILE_KEY, None)
if logging_section_file is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have a config section {LOGGING_FILE_KEY} under section {LOGGING_CONFIG_SECTION_KEY}.")
errors_section = self.config_dictionary.get(ERRORS_SECTION_KEY, None)
if errors_section is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have a config section {ERRORS_SECTION_KEY}.")
contact_name = errors_section.get(CONTACT_NAME_KEY, None)
if contact_name is None:
raise KeyError(
f"The dashboard configuration file {self.filename} did not have a config section {CONTACT_NAME_KEY} under {ERRORS_SECTION_KEY}.")
contact_method = errors_section.get(CONTACT_METHOD_KEY, None)
if contact_method is None:
raise KeyError(f"The dashboard configuration file {self.filename} did not have a config section {CONTACT_METHOD_KEY} under {ERRORS_SECTION_KEY}.")
except FileNotFoundError:
self.config_dictionary = {}

View File

@@ -0,0 +1,44 @@
from typing import TypedDict
import pandas as pd
from plotly.graph_objects import Figure
class FigureWithMaxY(TypedDict):
"""
A helper container class that gets used by report pages to encapsulate
Plotly figures along with the maximum y value represented on the figures.
This aids in axis synchronization when two of the same report are shown side by side
"""
figure:Figure
max_y:float
def find_fig_max_y_and_generate_wrapper(fig: Figure) -> 'FigureWithMaxY':
"""
Helper to automatically extract the max Y value from a Plotly figure
and wrap it in the FigureWithMaxY utility class.
"""
max_y = 0.0
# Iterate through all traces (bars, lines, etc.) in the figure
for trace in fig.data:
if hasattr(trace, 'y') and trace.y is not None:
# Get the max of this trace, ignoring NaNs
current_max = max(trace.y) if len(trace.y) > 0 else 0
max_y = max(max_y, float(current_max))
return FigureWithMaxY(figure=fig, max_y=max_y)
def extract_figure_data(figure:Figure) -> pd.DataFrame:
data_list = []
for trace in figure.data:
df_trace = pd.DataFrame({
'x': trace.x,
'y': trace.y
})
data_list.append(df_trace)
if len(data_list) == 0:
return pd.DataFrame()
full_df = pd.concat(data_list)
return full_df