carpet-efficiency / processor.py
DPPATRA's picture
Upload 11 files
78f194c verified
raw
history blame
11.5 kB
# processor.py
"""
Main processing orchestrator that ties together all the manufacturing priority logic.
This module provides high-level functions that both the CLI and Gradio interfaces can use.
"""
import os
import pandas as pd
from typing import Dict, List, Tuple, Optional
from datetime import datetime
from config import WEIGHTS
from sheet_reader import list_sheets, read_sheet
from priority_logic import compute_priority
from output_writer import save_with_instructions
from utils import prompt_weights
class ManufacturingProcessor:
"""
Main processor class for manufacturing priority calculations.
Encapsulates all the logic needed to process Excel files and generate priority rankings.
"""
def __init__(self, weights: Optional[Dict[str, int]] = None):
"""Initialize processor with weights"""
self.weights = weights or WEIGHTS.copy()
self.validate_weights()
def validate_weights(self) -> None:
"""Ensure weights sum to 100"""
total = sum(self.weights.values())
if total != 100:
raise ValueError(f"Weights must sum to 100, got {total}")
def get_file_info(self, file_path: str) -> Dict:
"""Get information about the Excel file"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"File not found: {file_path}")
try:
sheets = list_sheets(file_path)
file_size = os.path.getsize(file_path)
return {
"file_path": file_path,
"file_name": os.path.basename(file_path),
"file_size": file_size,
"sheets": sheets,
"sheet_count": len(sheets)
}
except Exception as e:
raise Exception(f"Error reading file info: {e}")
def validate_sheet_data(self, df: pd.DataFrame) -> Dict:
"""Validate that the sheet has required columns and data"""
from priority_logic import REQUIRED_COLS
# Normalize column names
df_norm = df.copy()
df_norm.columns = [str(c).strip() for c in df_norm.columns]
# Check required columns
missing_cols = [col for col in REQUIRED_COLS if col not in df_norm.columns]
# Basic data validation
validation_result = {
"valid": len(missing_cols) == 0,
"missing_columns": missing_cols,
"available_columns": list(df.columns),
"row_count": len(df),
"empty_rows": df.isnull().all(axis=1).sum(),
"data_issues": []
}
if validation_result["valid"]:
# Check for data quality issues
try:
# Check date column
date_col = "Oldest Product Required First"
date_issues = pd.to_datetime(df_norm[date_col], errors='coerce').isnull().sum()
if date_issues > 0:
validation_result["data_issues"].append(f"{date_issues} invalid dates in '{date_col}'")
# Check quantity column
qty_col = "Quantity of Each Component"
qty_numeric = pd.to_numeric(df_norm[qty_col], errors='coerce')
qty_issues = qty_numeric.isnull().sum()
if qty_issues > 0:
validation_result["data_issues"].append(f"{qty_issues} non-numeric values in '{qty_col}'")
# Check for completely empty required columns
for col in REQUIRED_COLS:
if col in df_norm.columns:
empty_count = df_norm[col].isnull().sum()
if empty_count == len(df_norm):
validation_result["data_issues"].append(f"Column '{col}' is completely empty")
except Exception as e:
validation_result["data_issues"].append(f"Data validation error: {e}")
return validation_result
def process_file(self,
file_path: str,
sheet_name: str,
min_qty: int = 50,
custom_weights: Dict[str, int] = None) -> Tuple[pd.DataFrame, Dict]:
"""
Process a single sheet from an Excel file and return prioritized results.
Returns:
Tuple of (processed_dataframe, processing_info)
"""
# Use custom weights if provided
weights = custom_weights or self.weights
if custom_weights:
temp_weights = custom_weights.copy()
if sum(temp_weights.values()) != 100:
raise ValueError("Custom weights must sum to 100")
else:
temp_weights = weights
# Read the data
df = read_sheet(file_path, sheet_name)
if df is None or df.empty:
raise ValueError("Sheet is empty or could not be read")
# Validate data
validation = self.validate_sheet_data(df)
if not validation["valid"]:
raise ValueError(f"Data validation failed: Missing columns {validation['missing_columns']}")
# Process priority calculation
try:
processed_df = compute_priority(df, min_qty=min_qty, weights=temp_weights)
except Exception as e:
raise Exception(f"Priority calculation failed: {e}")
# Generate processing info
processing_info = {
"timestamp": datetime.now().isoformat(),
"file_name": os.path.basename(file_path),
"sheet_name": sheet_name,
"weights_used": temp_weights,
"min_quantity": min_qty,
"total_products": len(df),
"products_above_threshold": sum(processed_df["QtyThresholdOK"]),
"highest_priority_score": processed_df["PriorityScore"].max(),
"lowest_priority_score": processed_df["PriorityScore"].min(),
"validation_info": validation
}
return processed_df, processing_info
def save_results(self,
processed_df: pd.DataFrame,
output_path: str,
processing_info: Dict) -> str:
"""Save processed results with full documentation"""
try:
save_with_instructions(
processed_df,
output_path,
min_qty=processing_info["min_quantity"],
weights=processing_info["weights_used"]
)
# Add processing log sheet
self._add_processing_log(output_path, processing_info)
return output_path
except Exception as e:
raise Exception(f"Failed to save results: {e}")
def _add_processing_log(self, output_path: str, processing_info: Dict):
"""Add a processing log sheet to the output file"""
try:
# Read existing file and add log sheet
with pd.ExcelWriter(output_path, mode='a', engine='openpyxl', if_sheet_exists='replace') as writer:
log_data = []
log_data.append(["PROCESSING LOG"])
log_data.append([""])
log_data.append(["Processing Timestamp", processing_info["timestamp"]])
log_data.append(["Source File", processing_info["file_name"]])
log_data.append(["Sheet Processed", processing_info["sheet_name"]])
log_data.append([""])
log_data.append(["SETTINGS USED"])
log_data.append(["Age Weight", f"{processing_info['weights_used']['AGE_WEIGHT']}%"])
log_data.append(["Component Weight", f"{processing_info['weights_used']['COMPONENT_WEIGHT']}%"])
log_data.append(["Manual Weight", f"{processing_info['weights_used']['MANUAL_WEIGHT']}%"])
log_data.append(["Minimum Quantity", processing_info["min_quantity"]])
log_data.append([""])
log_data.append(["RESULTS SUMMARY"])
log_data.append(["Total Products", processing_info["total_products"]])
log_data.append(["Above Threshold", processing_info["products_above_threshold"]])
log_data.append(["Highest Priority Score", f"{processing_info['highest_priority_score']:.4f}"])
log_data.append(["Lowest Priority Score", f"{processing_info['lowest_priority_score']:.4f}"])
if processing_info["validation_info"]["data_issues"]:
log_data.append([""])
log_data.append(["DATA ISSUES FOUND"])
for issue in processing_info["validation_info"]["data_issues"]:
log_data.append(["", issue])
log_df = pd.DataFrame(log_data, columns=["Parameter", "Value"])
log_df.to_excel(writer, sheet_name='Processing_Log', index=False)
except Exception as e:
# If adding log fails, don't fail the whole operation
print(f"Warning: Could not add processing log: {e}")
# Convenience functions for easy import
def quick_process(file_path: str,
sheet_name: str,
output_path: str = None,
min_qty: int = 50,
weights: Optional[Dict[str, int]] = None) -> str:
"""
Quick processing function that handles the full workflow.
Args:
file_path: Path to Excel file
sheet_name: Name of sheet to process
output_path: Where to save results (optional, will auto-generate if not provided)
min_qty: Minimum quantity threshold
weights: Custom weights dict (optional)
Returns:
Path to generated output file
"""
processor = ManufacturingProcessor(weights)
# Process the data
processed_df, processing_info = processor.process_file(
file_path, sheet_name, min_qty, weights
)
# Generate output path if not provided
if output_path is None:
base_name = os.path.splitext(os.path.basename(file_path))[0]
output_dir = os.path.dirname(file_path)
output_path = os.path.join(output_dir, f"{base_name}_PRIORITY.xlsx")
# Save results
return processor.save_results(processed_df, output_path, processing_info)
def get_file_preview(file_path: str, sheet_name: str, max_rows: int = 5) -> Dict:
"""
Get a preview of the file data for validation purposes.
Returns:
Dict containing preview info and sample data
"""
processor = ManufacturingProcessor()
# Get file info
file_info = processor.get_file_info(file_path)
# Read sample data
df = read_sheet(file_path, sheet_name)
sample_df = df.head(max_rows) if df is not None else pd.DataFrame()
# Validate data
validation = processor.validate_sheet_data(df) if df is not None else {"valid": False}
return {
"file_info": file_info,
"sample_data": sample_df,
"validation": validation,
"preview_rows": len(sample_df)
}