Parses portfolio related IL outputs to Excel
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.
PortfolioParser/IL Formatter.py

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()