diff --git a/README.md b/README.md index 73aa38ea9..8dcc80d60 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,12 @@ Run `python core.py validate --help` to see the list of validation options. -o, --output TEXT Report output file destination and name. Path will be relative to the validation execution directory and should end in the desired output filename - without file extension - '/user/reports/result' will be 'user/report' directory - with the filename as 'result' + without file extension. The file extension will be + automatically added based on the output format. + Example: 'reports/result' will create 'reports' directory + with the filename 'result.json' (or 'result.xlsx' for Excel). + Note: Provide a valid, writable path. Absolute paths must + be valid for your operating system. -of, --output-format [JSON|XLSX] Output file format -rr, --raw-report Report in a raw format as it is generated by diff --git a/cdisc_rules_engine/constants/__init__.py b/cdisc_rules_engine/constants/__init__.py index 0453d6f1f..e61d21310 100644 --- a/cdisc_rules_engine/constants/__init__.py +++ b/cdisc_rules_engine/constants/__init__.py @@ -15,3 +15,5 @@ ) NULL_FLAVORS = ["", None, {None}, [], {}, np.nan] + +KNOWN_REPORT_EXTENSIONS = [".json", ".xlsx", ".xls"] diff --git a/cdisc_rules_engine/services/reporting/base_report.py b/cdisc_rules_engine/services/reporting/base_report.py index 3fc37311a..16b5180eb 100644 --- a/cdisc_rules_engine/services/reporting/base_report.py +++ b/cdisc_rules_engine/services/reporting/base_report.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from io import IOBase +from cdisc_rules_engine.constants import KNOWN_REPORT_EXTENSIONS from cdisc_rules_engine.models.validation_args import Validation_args from cdisc_rules_engine.services.reporting.base_report_data import ( BaseReportData, @@ -21,7 +22,23 @@ def __init__( self._report_standard = report_standard self._args = args self._template = template - self._output_name: str = f"{self._args.output}.{self._file_ext}" + self._output_name: str = self._get_output_filename() + + def _get_output_filename(self) -> str: + expected_ext = f".{self._file_ext}" + output_path = self._args.output + + if output_path.lower().endswith(expected_ext.lower()): + base_path = output_path[: -len(expected_ext)] + return f"{base_path}{expected_ext}" + + path_lower = output_path.lower() + for ext in KNOWN_REPORT_EXTENSIONS: + if path_lower.endswith(ext): + base_path = output_path[: -len(ext)] + return f"{base_path}{expected_ext}" + + return f"{output_path}{expected_ext}" @property @abstractmethod diff --git a/cdisc_rules_engine/services/reporting/excel_report.py b/cdisc_rules_engine/services/reporting/excel_report.py index 6011c58fb..a40d27a4a 100644 --- a/cdisc_rules_engine/services/reporting/excel_report.py +++ b/cdisc_rules_engine/services/reporting/excel_report.py @@ -85,9 +85,22 @@ def write_report(self): logger = logging.getLogger("validator") try: report_data = self.get_export() + output_dir = os.path.dirname(self._output_name) + if output_dir: + try: + os.makedirs(output_dir, exist_ok=True) + except OSError as e: + error_msg = ( + f"Cannot create output directory '{output_dir}': {e.strerror}. " + f"Please provide a valid, writable path for the output file." + ) + logger.error(error_msg) + raise OSError(error_msg) from e with open(self._output_name, "wb") as f: f.write(excel_workbook_to_stream(report_data)) logger.debug(f"Report written to: {self._output_name}") + except OSError: + raise except Exception as e: logger.error(f"Error writing report: {e}") raise e diff --git a/cdisc_rules_engine/services/reporting/json_report.py b/cdisc_rules_engine/services/reporting/json_report.py index 4aadf7a56..3c1a32b2b 100644 --- a/cdisc_rules_engine/services/reporting/json_report.py +++ b/cdisc_rules_engine/services/reporting/json_report.py @@ -1,4 +1,5 @@ import json +import os from typing import BinaryIO, override from cdisc_rules_engine.enums.report_types import ReportTypes @@ -66,5 +67,14 @@ def write_report(self): report_data = self.get_export( raw_report=self._args.raw_report, ) + output_dir = os.path.dirname(self._output_name) + if output_dir: + try: + os.makedirs(output_dir, exist_ok=True) + except OSError as e: + raise OSError( + f"Cannot create output directory '{output_dir}': {e.strerror}. " + f"Please provide a valid, writable path for the output file." + ) from e with open(self._output_name, "w") as f: json.dump(report_data, f) diff --git a/scripts/run_validation.py b/scripts/run_validation.py index 3e08fddfc..5b6e427d6 100644 --- a/scripts/run_validation.py +++ b/scripts/run_validation.py @@ -199,9 +199,14 @@ def run_validation(args: Validation_args): datasets, results, elapsed_time, args, data_service, dictionary_versions ) reporting_services: List[BaseReport] = reporting_factory.get_report_services() + output_files = [] for reporting_service in reporting_services: reporting_service.write_report() - print(f"Output: {args.output}") + output_files.append(reporting_service._output_name) + if len(output_files) == 1: + print(f"Output: {output_files[0]}") + else: + print(f"Output: {', '.join(output_files)}") finally: if created_files: engine_logger.info(" Report generated, Cleaning up intermediate files")