from ui import Ui_MainWindow from errorDialog import ErrorDialog from PyQt5 import QtWidgets from logging import debug, info, warning, exception as logException, error,DEBUG, INFO, WARNING, ERROR, basicConfig, getLogger from sys import argv from typing import Literal, Optional from pandas import DataFrame, ExcelWriter from datetime import datetime as dt from os import startfile from json import load, dump from time import sleep from ILParser import InfoTreieveReport, FlippedReport # Open the config file, create a dict, and set up logging with open("config.json") as configFile: config: dict = load(configFile) basicConfig(filename='ILFormatter.log', encoding='utf-8', level=config["loggingLevel"], filemode='w', force=True) info(f"Starting with log level: {getLogger().level}") # Change the current log level and save the change to config.json def change_log_level(newLevel: Literal["ERROR", "WARNING", "INFO", "DEBUG"]): """ Changes the logging level of the logger and updates the configuration file. Args: newLevel (Literal["ERROR", "WARNING", "INFO", "DEBUG"]): The new logging level to set. """ # Update the logging level in the configuration dictionary and save to file config["loggingLevel"] = newLevel with open("config.json", 'w') as configFile: dump(config, configFile) # Set the logging level of the logger getLogger().setLevel(newLevel) # Print a message to indicate the new logging level print(f"{now()} | New logging level: {getLogger().level}\n") # Creates an error dialog pop up # Based on the ui from errorDialog.py def open_error_dialog(errorLabel: str, errorDescription: str, errorText: str): dialog = QtWidgets.QDialog() dialog.ui = ErrorDialog() dialog.ui.setupUi(dialog) dialog.ui.setFields(errorLabel, errorDescription, errorText) dialog.exec_() # Used to easily record uniform timestamps now = lambda : dt.now().strftime("%H:%M-%S.%f") fileRoot = lambda filePath : '/'.join(filePath.split('/')[:-1]) # This class is responable for managing the UI of the application # and connection it to the functionality of ILParser.py class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self, *args, obj=None, **kwargs): debug("MainWindow class init..") super(MainWindow, self).__init__(*args, **kwargs) self.setupUi(self) self.processButton.setEnabled(False) self.openButton.setEnabled(False) # Fix log level action buttons level: QtWidgets.QAction for level in [self.actionError, self.warnAction, self.infoAction, self.debugAction]: inUse = level.text().upper() == config["loggingLevel"] level.setEnabled(not inUse) level.setChecked(inUse) if inUse: self.logLevel = level debug(f"Logging Level: {self.logLevel}") # File Locations self.assetFile = None self.custFile = None self.dobFile = None self.finFile = None self.outputLocation = None # Button Hooks self.assetButton.clicked.connect(lambda: self._set_file(lineEdit= self.assetLE, selfFile="ASSET")) self.custButton.clicked.connect(lambda: self._set_file(lineEdit= self.custLe, selfFile="CUST")) self.dobButton.clicked.connect(lambda: self._set_file(lineEdit= self.dobLE, selfFile="DOB")) self.finButton.clicked.connect(lambda: self._set_file(lineEdit= self.finLE, selfFile="FIN")) self.outputButton.clicked.connect(lambda: self._set_output()) self.processButton.clicked.connect(lambda: self._process()) self.openButton.clicked.connect(lambda: self._open_with_default_app(self.outputLocation)) # Action Hooks self.actionError.triggered.connect(lambda: self._switch_log_levels(self.actionError)) self.warnAction.triggered.connect(lambda: self._switch_log_levels(self.warnAction)) self.infoAction.triggered.connect(lambda: self._switch_log_levels(self.infoAction)) self.debugAction.triggered.connect(lambda: self._switch_log_levels(self.debugAction)) def _check_files(self): """ Checks if all the required files have been loaded and the output location has been specified. Enables or disables the open button and process button accordingly. """ # Print the paths of the loaded files for debugging debug(self.assetFile) debug(self.custFile) debug(self.dobFile) debug(self.finFile) # Disable the open button self.openButton.setEnabled(False) # Check if all required files and output location have been loaded ready = ( self.assetFile is not None and self.custFile is not None and self.dobFile is not None and self.finFile is not None and self.outputLocation is not None ) # Enable or disable the process button based on readiness self.processButton.setEnabled(ready) def _set_file(self, lineEdit: QtWidgets.QLineEdit, selfFile: Literal["ASSET", "CUST", "DOB", "FIN"]) -> Optional[str]: """ Sets a file location based on user input using a file dialog. Args: lineEdit (QtWidgets.QLineEdit): The line edit object that displays the selected file location. selfFile (Literal["ASSET", "CUST", "DOB", "FIN"]): The file type being selected. Returns: Optional[str]: The selected file location, or None if no file is selected. """ # Get the default location based on the file type defaultLocation = self._default_location(selfFile) # Show the file dialog and get the selected file path selectedFile: list[str] = QtWidgets.QFileDialog.getOpenFileName(self, "OpenFile", directory=defaultLocation) debug(f"Selected file: {selectedFile}") # Set the text of the line edit to the selected file path lineEdit.setText(selectedFile[0]) # Save the selected file location for future reference file = selectedFile[0] if selectedFile[0] != '' else None if file!= None: self._default_location(selfFile, set=fileRoot(file)) # If the output location has not been set yet, set it to the file root of the selected file if file != None and self.outputLocation == None: self._auto_output_set(fileRootStr=fileRoot(file)) # Set the correct file variable based on the file type if selfFile == "ASSET": self.assetFile = file elif selfFile == "CUST": self.custFile = file elif selfFile == "DOB": self.dobFile = file elif selfFile == "FIN": self.finFile = file # Check if all required files have been selected and enable the "process" button if so self._check_files() def _set_output(self): """ Set the output file location using a file dialog and update the UI accordingly. Also, update the default location for output in the settings. :return: None """ # Show a file dialog to get the save file name for the output self.outputLocation = QtWidgets.QFileDialog.getSaveFileName(self, "Output file name") if QtWidgets.QFileDialog.getSaveFileName(self, "Output file name") != '' else None # Update the output line edit in the UI with the selected file location or an empty string if no file was selected self.outputLE.setText(self.outputLocation if self.outputLocation != None else '') # Update the default output location in the settings, overwriting the existing value self._default_location("output", set=fileRoot(self.outputLocation), overwrite=True) # Log the selected output location debug(f"Output Location: {self.outputLocation}") def _default_location(self, file: Literal["ASSET", "CUST", "DOB", "FIN", "output"], set: str = None, overwrite: bool = False) -> Optional[str]: """ Get or set the default location for a specified file. If 'set' is provided and the default location is not set, or 'overwrite' is True, the default location will be updated. :param file: The file type, one of ["ASSET", "CUST", "DOB", "FIN", "output"]. :param set: The new default location, if setting or updating it. :param overwrite: Whether to overwrite the existing default location. :return: The default location for the specified file. """ # Get the default location for the specified file defaultLocation = config["directories"][file] debug(f"Default location: {defaultLocation}") if set != None: debug(f"Setting default location to: {set} | ({(overwrite | (defaultLocation == None))})") # Set or overwrite the default location if conditions are met config["directories"][file] = set if (overwrite | (defaultLocation == None)) else defaultLocation # Save the updated configuration with open("config.json", 'w') as configFile: dump(config, configFile) return set else: return defaultLocation def _auto_output_set(self, fileRootStr): """ Automatically set the output file location based on the saved configuration. :param fileRootStr: The root directory for the output file. :return: None """ # Create the output file location string self.outputLocation = fileRootStr + f"/Portfolio Contracts - {dt.now().strftime('%Y-%m-%d')}.xlsx" # Update the default output location in the settings, without overwriting self._default_location("output", set=fileRoot(self.outputLocation), overwrite=False) # Update the output line edit in the UI with the auto-generated file location self.outputLE.setText(self.outputLocation if self.outputLocation != None else '') debug(f"Auto set output: {self.outputLocation}") def _open_with_default_app(self, item): """ Open the specified item with its default application (e.g. Excel). :param item: The item to be opened with the default application. :return: None """ debug(f"_open_with_default_app: {item}") startfile(item) def _switch_log_levels(self, newLevel: QtWidgets.QAction): """ Switch the log level for the application, updating the log level QAction objects accordingly. :param newLevel: The new log level QAction to be set. :return: None """ # Store the current log level QAction oldLevel: QtWidgets.QAction = self.logLevel print(f"{now()} | Log Level Changed: {oldLevel.text()} -> {newLevel.text()}") # Update the QAction objects' state newLevel.setChecked(True) oldLevel.setChecked(False) newLevel.setEnabled(False) oldLevel.setEnabled(True) # Update the log level reference in the class self.logLevel = newLevel # Change the log level in the logging system change_log_level(newLevel.text().upper()) def _parse_file(self, filePath: str) -> Optional[DataFrame]: """ Parse a file at the given file path and extract data into a DataFrame using the ILParser. :param filePath: The path to the file to be parsed. :return: A DataFrame containing the extracted data, or None if parsing failed or the DataFrame is empty. """ with open(filePath) as file: report = file.read() debug(f"Report: {report}") debug(f"Parse Columns:\n{config['COLS']}") try: it_report: InfoTreieveReport = InfoTreieveReport(report, config["COLS"]) data: DataFrame = it_report.process() except Exception as e: self.processButton.setEnabled(False) logException(f"Failed to parse file-> {filePath} :\n{e}") open_error_dialog("Parsing Error:", f"Failed to parse file-> {filePath}", repr(e)) return None debug(f"Data: {data}") if data.empty: error(f"Dataframe empty -> {filePath} | Returning none") open_error_dialog("Data Processing Error:", f"Dataframe empty!", filePath) return None else: return data def _process(self): """ Process the input files, parse their data, and write the results to an Excel file. :return: None """ assetDf: Optional[DataFrame] = self._parse_file(filePath=self.assetFile) debug(f"AssetDF: {assetDf} | {type(assetDf)} ") if type(assetDf) != DataFrame: self.assetLE.setText("") self.assetFile = None return None custDf: DataFrame = self._parse_file(self.custFile) debug(custDf) if type(custDf) != DataFrame: self.custLe.setText("") self.custFile = None #FIXME return None dobDf: DataFrame = self._parse_file(self.dobFile) debug(dobDf) if type(dobDf) != DataFrame: self.dobLE.setText("") self.dobFile = None return None finDf: DataFrame = self._parse_file(self.finFile) debug(finDf) if type(finDf) != DataFrame: self.finLE.setText("") self.finFile = None #FIXME return None bad_cols: list[str] = config['COLS'] successful_new: dict[str:DataFrame] = {} for id, report_file in enumerate([self.custFile, self.finFile]): try: with open(report_file) as file: report = file.read() flipped_report: FlippedReport = FlippedReport(report, bad_cols) flipped_df: DataFrame = flipped_report.process() if not flipped_df.empty: successful_new["NEW_CUST" if id == 0 else "NEW_FIN"] = flipped_df except: pass try: with ExcelWriter(self.outputLocation) as writer: finDf.to_excel(writer, sheet_name="FIN", index=False) custDf.to_excel(writer, sheet_name="CUST", index=False) assetDf.to_excel(writer, sheet_name="ASSET", index=False) dobDf.to_excel(writer, sheet_name="DOB", index=False) key: str df: DataFrame for key, df in successful_new.items(): df.to_excel(writer, sheet_name=key, index=False) except Exception as e: logException(f"{now()} | Failed to write to excel -> {self.outputLocation} :\n{e}") open_error_dialog("Failed to Create Excel", f"Failed to write to excel -> {self.outputLocation}", repr(e)) return None debug("Finished writing to excel.") self.openButton.setEnabled(True) # Defines the app app = QtWidgets.QApplication(argv) # Sets the style app.setStyle("Fusion") # Builds the main window window = MainWindow() window.setWindowTitle("IL Extract") window.show() # Starts the app app.exec()