/
Flipper
/
Marginal
Обзор
Документация
Войти
/
Flipper
/
Marginal
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
master
src/FileManager.py
552 строки
17 KB
Alex
Bug fixes.
12 янв 2026, 13:17
12 янв 2026, 13:17
e907759
Код
Авторство
О чём код?
# FileManager.py import json import csv import os import sys import subprocess import threading from datetime import datetime import utils from openpyxl import load_workbook, Workbook from openpyxl.styles import Font from openpyxl.utils import range_boundaries class FileManager: """ Handles all file input/output operations including: - Loading profiles - Reading Excel input data - Exporting results to Excel or CSV - Exporting graphs - File naming and background opening """ def __init__(self, backbone): """ Initialize FileManager with application backbone. Args: backbone (Backbone): Central application controller. """ self.backbone = backbone self.logger = self.backbone.logger self.defaultFilename = None self.csvDelimiter = None self.graphExportExt = None self.useConfig() def useConfig(self): """ Load export-related configuration from the backbone. Returns: None """ exportConfig = self.backbone.getConfig("exportOptions") self.defaultFilename = exportConfig["outputFilename"] self.csvDelimiter = exportConfig["csvDelimiter"] self.graphExportExt = self.backbone.getConfig("graphOptions")["extension"] def loadProfile(self, path): """ Load a JSON profile file. Args: path (str): Path to the JSON profile. Returns: dict -> Parsed JSON profile data. Raises: IOError: If the file cannot be read. json.JSONDecodeError: If the file is not valid JSON. """ with open(path, "r", encoding="utf-8") as file: return json.load(file) def readExcelData(self, filePath, inputs): """ Read input values from an Excel file based on input definitions. Args: filePath (str): Path to the Excel file. inputs (dict): Input definitions with cell references. Returns: dict -> Mapping of input keys to extracted values. Raises: KeyError: If referenced sheets do not exist. """ workbook = load_workbook(filePath, data_only=True) defaultSheet = workbook.active data = {} for key, info in inputs.items(): source = info["source"] precision = info.get("precision") if "!" in source: sheetName, cellRef = source.split("!") worksheet = workbook[sheetName] else: worksheet = defaultSheet cellRef = source if ":" in cellRef: values = [ self._toNumber(cell.value) for row in worksheet[cellRef] for cell in row ] if precision is not None: values = [ round(v, precision) if isinstance(v, (int, float)) else v for v in values ] data[key] = values else: value = self._toNumber(worksheet[cellRef].value) if precision is not None and isinstance(value, (int, float)): value = round(value, precision) data[key] = value return data def writeExcel(self, outputs, results, exportPath, filename, textHeaders=None): """ Export calculation results to an Excel file. Args: outputs (dict): Output metadata definitions. results (dict): Computed result values. exportPath (str): Relative export directory. filename (str): Output filename (without extension). textHeaders (dict | None): Optional static text headers. Returns: str -> Full path to the saved Excel file. Raises: IOError: If file saving fails. """ workbook = Workbook() workbook.remove(workbook.active) worksheets = {} collectedHeaders = {} for key, info in outputs.items(): headerInfo = info.get("textHeader") if headerInfo: collectedHeaders[f"{key}_header"] = { "placement": headerInfo.get("placement", "A1"), "text": headerInfo.get("text", ""), "bold": headerInfo.get("bold", False), } headersToWrite = textHeaders if textHeaders else collectedHeaders if isinstance(headersToWrite, dict): for header in headersToWrite.values(): self._writeTextToCell( workbook, header["placement"], header["text"], header.get("bold", False), worksheets, ) hasPlacement = any("placement" in info for info in outputs.values()) if hasPlacement: self._writeWithPlacement(workbook, outputs, results, worksheets) else: worksheet = worksheets.setdefault( "Results", workbook.create_sheet("Results") ) self._writeDefaultFormat(worksheet, outputs, results) fullPath = utils.generateFullPath( utils.getBasePath(), f"{exportPath}/{filename}.xlsx" ).replace("\\", "/") os.makedirs(os.path.dirname(fullPath), exist_ok=True) workbook.save(fullPath) self.logger.customLog( "->Results exported to {exportPath}", translate=True, exportPath=fullPath, ) return fullPath def writeCSV(self, outputs, results, exportPath, filename, textHeaders=None): """ Export calculation results to a CSV file. Args: outputs (dict): Output metadata definitions. results (dict): Computed result values. exportPath (str): Relative export directory. filename (str): Output filename (without extension). textHeaders (dict | None): Optional static text headers. Returns: str | None -> Full path to CSV file or None on failure. """ try: fullPath = utils.generateFullPath( utils.getBasePath(), f"{exportPath}/{filename}.csv" ) os.makedirs(os.path.dirname(fullPath), exist_ok=True) with open(fullPath, "w", newline="", encoding="utf-8") as csvFile: writer = csv.writer(csvFile, delimiter=self.csvDelimiter) if textHeaders: writer.writerow([f"# Text Headers: {len(textHeaders)} items"]) for header in textHeaders.values(): writer.writerow( [ f"# {header.get('placement', '')}: " f"{header.get('text', '')}" ] ) writer.writerow([]) writer.writerow(["Output Name", "Value", "Unit", "Description"]) for key, info in outputs.items(): value = results.get(key, "N/A") description = info.get("description", "") if isinstance(value, list): for i, item in enumerate(value): if isinstance(item, (int, float)): precision = info.get("precision") if precision is not None: item = round(item, precision) writer.writerow( [ key if i == 0 else "", item, info.get("unit", ""), description if i == 0 else "", ] ) else: if isinstance(value, (int, float)): precision = info.get("precision") if precision is not None: value = round(value, precision) writer.writerow( [key, value, info.get("unit", ""), description] ) self.logger.customLog( "->Results exported to {exportPath}", translate=True, exportPath=fullPath, ) return fullPath except Exception as exc: self.logger.customLog( "->CSV export failed: {e}", translate=True, e=str(exc) ) return None def writeOutputs( self, outputs, results, exportPath, exportMode, filename, openOnFinish, textHeaders=None, ): """ Dispatch export operation based on selected format. Args: outputs (dict): Output definitions. results (dict): Computed results. exportPath (str): Relative export directory. exportMode (str): File extension ('.xlsx' or '.csv'). filename (str): Output filename. openOnFinish (bool): Whether to open the file after export. textHeaders (dict | None): Optional static headers. Returns: None """ path = None if exportMode == ".xlsx": path = self.writeExcel(outputs, results, exportPath, filename, textHeaders) elif exportMode == ".csv": path = self.writeCSV(outputs, results, exportPath, filename, textHeaders) else: self.logger.customLog( "->Export failed - unrecognised export type: {exportMode}", translate=True, exportMode=exportMode, ) if path and openOnFinish: self.openFileBackground(path) def saveGraph(self, fig, exportPath, filename): """ Save a matplotlib figure to disk. Args: fig (matplotlib.figure.Figure): Figure to save. exportPath (str): Relative export directory. filename (str): Output filename (without extension). Returns: str | None -> Full path to saved graph or None on failure. """ allowedExt = [".pdf", ".png"] if self.graphExportExt not in allowedExt: self.logger.customLog( "->Graph export failed - unrecognised export type: {exportExt}", translate=True, exportExt=self.graphExportExt, ) return None try: fullPath = utils.generateFullPath( utils.getBasePath(), f"{exportPath}/{filename}{self.graphExportExt}", ) fig.savefig( fullPath, format=self.graphExportExt.strip("."), dpi=300, bbox_inches="tight", ) return fullPath except Exception as exc: self.logger.customLog( "->Graph export failed: {e}", translate=True, e=str(exc) ) return None def generateFileName(self, entryString): """ Generate a filename using timestamp formatting rules. Args: entryString (str): Filename template or raw name. Returns: str -> Generated filename. """ timestamp = datetime.now() if not entryString: entryString = timestamp.strftime(self.defaultFilename) if entryString.count("$") % 2 == 0: return timestamp.strftime(entryString) return entryString def openFileBackground(self, filePath): """ Open a file in the system default application asynchronously. Args: filePath (str): Path to the file. Returns: None """ def openFile(): try: if sys.platform == "win32": os.startfile(filePath) elif sys.platform == "darwin": subprocess.run(["open", filePath]) else: subprocess.run(["xdg-open", filePath]) except Exception as exc: self.logger.log("FileManager", "Failed to open file: {e}", e=exc) threading.Thread(target=openFile, daemon=True).start() def _writeTextToCell(self, workbook, placement, text, bold=False, worksheets=None): """ Write static text to a specific cell or range. Args: workbook (Workbook): Target workbook. placement (str): Sheet and cell reference. text (str): Text to write. bold (bool): Apply bold formatting. worksheets (dict | None): Cached worksheets. Returns: None """ worksheets = worksheets or {} sheetName = "Results" cellRef = placement if "!" in placement: sheetName, cellRef = placement.split("!") # FIXED: Check if worksheet already exists before creating a new one if sheetName in workbook.sheetnames: # Sheet already exists, get it from workbook worksheet = workbook[sheetName] worksheets[sheetName] = worksheet elif sheetName in worksheets: # Sheet exists in our cache worksheet = worksheets[sheetName] else: # Create new worksheet worksheet = workbook.create_sheet(sheetName) worksheets[sheetName] = worksheet if ":" in cellRef: colStart, rowStart, _, _ = range_boundaries(cellRef) cell = worksheet.cell(row=rowStart, column=colStart) else: cell = worksheet[cellRef] cell.value = text if bold: cell.font = Font(bold=True) def _writeDefaultFormat(self, worksheet, outputs, results): """ Write outputs sequentially into a worksheet. Args: worksheet (Worksheet): Target worksheet. outputs (dict): Output definitions. results (dict): Computed values. Returns: None """ worksheet.append(["Output Name", "Value", "Unit", "Description"]) for key, info in outputs.items(): value = results.get(key, "N/A") description = info.get("description", "") if isinstance(value, list): for index, item in enumerate(value): worksheet.append( [ key if index == 0 else "", item, info.get("unit", ""), description if index == 0 else "", ] ) else: worksheet.append( [key, value, info.get("unit", ""), description] ) def _writeWithPlacement(self, workbook, outputs, results, worksheets=None): """ Write outputs using explicit cell placements. Args: workbook (Workbook): Target workbook. outputs (dict): Output definitions. results (dict): Computed values. worksheets (dict | None): Cached worksheets. Returns: None """ worksheets = worksheets or {} for key, info in outputs.items(): value = results.get(key, "N/A") placement = info.get("placement") if not placement: continue sheetName, cellRef = ( placement.split("!") if "!" in placement else ("Results", placement) ) # FIXED: Check if worksheet already exists before creating a new one if sheetName in workbook.sheetnames: # Sheet already exists, get it from workbook worksheet = workbook[sheetName] worksheets[sheetName] = worksheet elif sheetName in worksheets: # Sheet exists in our cache worksheet = worksheets[sheetName] else: # Create new worksheet worksheet = workbook.create_sheet(sheetName) worksheets[sheetName] = worksheet if isinstance(value, list) and ":" in cellRef: colStart, rowStart, _, rowEnd = range_boundaries(cellRef) for offset, item in enumerate(value): if rowStart + offset <= rowEnd: worksheet.cell( row=rowStart + offset, column=colStart, value=item ) else: if ":" in cellRef: colStart, rowStart, _, _ = range_boundaries(cellRef) worksheet.cell(row=rowStart, column=colStart, value=value) else: worksheet[cellRef] = value def _toNumber(self, value): """ Convert a value to float if possible. Args: value (any): Input value. Returns: float | None -> Parsed number or None if invalid. """ if value is None: return None if isinstance(value, (int, float)): return float(value) if isinstance(value, str): try: return float(value.strip().replace(",", ".")) except ValueError: return None return None