You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
339 lines
14 KiB
339 lines
14 KiB
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
|
|
|
|
import ILParser
|
|
|
|
|
|
# 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:
|
|
data: DataFrame = ILParser.extract_data(report, config["COLS"])
|
|
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
|
|
return None
|
|
|
|
dobDf: DataFrame = self._parse_file(self.dobFile)
|
|
debug(dobDf)
|
|
|
|
if type(dobDf) != DataFrame:
|
|
debug(f"Parse Columns: {ILParser.DOB_COL}")
|
|
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
|
|
return None
|
|
|
|
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)
|
|
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() |