diff --git a/model_manager/sientia/reports.py b/model_manager/sientia/reports.py index 7a12fee..dc66b59 100644 --- a/model_manager/sientia/reports.py +++ b/model_manager/sientia/reports.py @@ -55,6 +55,22 @@ def inject_content(main_html, section_id, content): class Reports: + """ + Report generator using Evidently library. + + Thread-safety: This class is NOT thread-safe. Multiple threads should not + call add_*_section() methods on the same instance simultaneously as they + modify shared state (self.metrics, self.sections, self.options). + + For multi-threaded environments: + - Create separate Reports instances per thread + - Or synchronize access using locks + - After generation, instances are safe for read-only operations + + I/O Note: This class relies on Evidently's report.save_html() method + for file operations. Ensure Evidently properly manages file handles. + """ + def __init__( self, reference_data: Any, current_data: Any, base_path: str | None = None ) -> None: @@ -95,6 +111,7 @@ class Reports: report.run(reference_data=self.ref_data, current_data=self.cur_data) self.sections['data_quality'] = report.as_dict() if self.base_path: + # Note: Relies on Evidently's save_html() to properly manage file I/O report.save_html(os.path.join(self.base_path, 'data_quality.html')) def add_data_drift_section(self, columns: list[str] | None = None, run: bool = True) -> None: @@ -111,6 +128,7 @@ class Reports: report.run(reference_data=self.ref_data, current_data=self.cur_data) self.sections['data_drift'] = report.as_dict() if self.base_path: + # Note: Relies on Evidently's save_html() to properly manage file I/O report.save_html(os.path.join(self.base_path, 'data_drift.html')) def add_regression_section(self, run: bool = True) -> None: @@ -135,6 +153,7 @@ class Reports: report.run(reference_data=self.ref_data, current_data=self.cur_data) self.sections['regression'] = report.as_dict() if self.base_path: + # Note: Relies on Evidently's save_html() to properly manage file I/O report.save_html(os.path.join(self.base_path, 'regression.html')) def set_color_options( @@ -201,10 +220,23 @@ class Reports: Args: report_path: The path to save the report HTML file. + + Raises: + ValueError: If base_path is not set + OSError: If directory creation or file writing fails + + Note: + This method uses context manager (with open) to ensure file is properly closed. + Creates parent directories if they don't exist. """ if not self.base_path: raise ValueError('base_path is required to save all sections HTML') + # Ensure output directory exists + output_dir = os.path.dirname(report_path) + if output_dir and not os.path.exists(output_dir): + os.makedirs(output_dir, exist_ok=True) + # Load main HTML template main_html_path = os.path.join(self.base_path, 'header.html') main_html = load_html_from_file(main_html_path) @@ -222,5 +254,6 @@ class Reports: main_html = inject_content(main_html, 'regression', regression_content) # Save the final HTML to a new file (report.html) + # Context manager ensures file is properly closed even if an error occurs with open(report_path, 'w', encoding='utf-8') as report_file: report_file.write(main_html)