Update app.py
Browse files
app.py
CHANGED
|
@@ -12,6 +12,23 @@ import tempfile
|
|
| 12 |
# Configure logging to match the log format
|
| 13 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s,%(msecs)03d - %(levelname)s - %(message)s')
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def process_files(uploaded_files):
|
| 16 |
"""
|
| 17 |
Process uploaded CSV files, generate usage plots, detect anomalies, and process AMC expiries.
|
|
@@ -39,6 +56,11 @@ def process_files(uploaded_files):
|
|
| 39 |
try:
|
| 40 |
df = pd.read_csv(file.name)
|
| 41 |
logging.info(f"Loaded {len(df)} records from {file.name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
all_data.append(df)
|
| 43 |
except Exception as e:
|
| 44 |
logging.error(f"Failed to load {file.name}: {str(e)}")
|
|
@@ -79,7 +101,7 @@ def process_files(uploaded_files):
|
|
| 79 |
# Prepare output dataframe (combine original data with anomalies)
|
| 80 |
output_df = combined_df.copy()
|
| 81 |
if anomaly_df is not None:
|
| 82 |
-
output_df['anomaly'] = anomaly_df['anomaly']
|
| 83 |
|
| 84 |
return output_df, plot_path, pdf_path, amc_message
|
| 85 |
|
|
@@ -89,20 +111,27 @@ def generate_usage_plot(df):
|
|
| 89 |
Returns the path to the saved plot.
|
| 90 |
"""
|
| 91 |
try:
|
| 92 |
-
plt.figure(figsize=(
|
|
|
|
|
|
|
| 93 |
for status in df['status'].unique():
|
| 94 |
subset = df[df['status'] == status]
|
| 95 |
-
plt.bar(
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
plt.tight_layout()
|
| 102 |
|
| 103 |
# Save plot to temporary file
|
| 104 |
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp:
|
| 105 |
-
plt.savefig(tmp.name, format='png')
|
| 106 |
plot_path = tmp.name
|
| 107 |
plt.close()
|
| 108 |
return plot_path
|
|
@@ -136,8 +165,8 @@ def process_amc_expiries(df):
|
|
| 136 |
df['amc_expiry'] = pd.to_datetime(df['amc_expiry'])
|
| 137 |
upcoming_expiries = df[df['amc_expiry'] <= threshold]
|
| 138 |
unique_devices = upcoming_expiries['equipment'].unique()
|
| 139 |
-
message = f"Found {len(unique_devices)} devices with upcoming AMC expiries."
|
| 140 |
-
logging.info(
|
| 141 |
return message, upcoming_expiries
|
| 142 |
except Exception as e:
|
| 143 |
logging.error(f"Failed to process AMC expiries: {str(e)}")
|
|
@@ -149,38 +178,59 @@ def generate_pdf_report(original_df, anomaly_df, amc_df):
|
|
| 149 |
Returns the path to the saved PDF.
|
| 150 |
"""
|
| 151 |
try:
|
| 152 |
-
if original_df is None:
|
| 153 |
logging.warning("No data available for PDF generation.")
|
| 154 |
return None
|
| 155 |
|
| 156 |
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp:
|
| 157 |
c = canvas.Canvas(tmp.name, pagesize=letter)
|
|
|
|
| 158 |
c.drawString(100, 750, "Equipment Log Analysis Report")
|
| 159 |
-
|
|
|
|
| 160 |
|
| 161 |
# Summary
|
|
|
|
|
|
|
| 162 |
c.drawString(100, y, f"Total Records: {len(original_df)}")
|
| 163 |
-
|
|
|
|
| 164 |
y -= 40
|
| 165 |
|
| 166 |
# Anomalies
|
|
|
|
|
|
|
| 167 |
if anomaly_df is not None:
|
| 168 |
num_anomalies = sum(anomaly_df['anomaly'] == -1)
|
| 169 |
c.drawString(100, y, f"Anomalies Detected: {num_anomalies}")
|
|
|
|
| 170 |
if num_anomalies > 0:
|
| 171 |
-
|
| 172 |
-
c.drawString(100, y
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
else:
|
| 175 |
c.drawString(100, y, "Anomaly detection failed.")
|
| 176 |
y -= 20
|
|
|
|
| 177 |
|
| 178 |
# AMC Expiries
|
|
|
|
|
|
|
| 179 |
if amc_df is not None and not amc_df.empty:
|
| 180 |
c.drawString(100, y, f"Devices with Upcoming AMC Expiries: {len(amc_df['equipment'].unique())}")
|
|
|
|
| 181 |
for _, row in amc_df.iterrows():
|
| 182 |
-
c.drawString(100, y
|
| 183 |
y -= 20
|
|
|
|
|
|
|
|
|
|
| 184 |
else:
|
| 185 |
c.drawString(100, y, "No AMC expiry data available.")
|
| 186 |
y -= 20
|
|
|
|
| 12 |
# Configure logging to match the log format
|
| 13 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s,%(msecs)03d - %(levelname)s - %(message)s')
|
| 14 |
|
| 15 |
+
def validate_csv(df):
|
| 16 |
+
"""
|
| 17 |
+
Validate that the CSV has the required columns.
|
| 18 |
+
Returns True if valid, False otherwise with an error message.
|
| 19 |
+
"""
|
| 20 |
+
required_columns = ['equipment', 'usage_count', 'status', 'amc_expiry']
|
| 21 |
+
missing_columns = [col for col in required_columns if col not in df.columns]
|
| 22 |
+
if missing_columns:
|
| 23 |
+
return False, f"Missing required columns: {', '.join(missing_columns)}"
|
| 24 |
+
# Validate data types
|
| 25 |
+
try:
|
| 26 |
+
df['usage_count'] = pd.to_numeric(df['usage_count'], errors='raise')
|
| 27 |
+
df['amc_expiry'] = pd.to_datetime(df['amc_expiry'], errors='raise')
|
| 28 |
+
except Exception as e:
|
| 29 |
+
return False, f"Invalid data types: {str(e)}"
|
| 30 |
+
return True, ""
|
| 31 |
+
|
| 32 |
def process_files(uploaded_files):
|
| 33 |
"""
|
| 34 |
Process uploaded CSV files, generate usage plots, detect anomalies, and process AMC expiries.
|
|
|
|
| 56 |
try:
|
| 57 |
df = pd.read_csv(file.name)
|
| 58 |
logging.info(f"Loaded {len(df)} records from {file.name}")
|
| 59 |
+
# Validate CSV structure
|
| 60 |
+
is_valid, error_msg = validate_csv(df)
|
| 61 |
+
if not is_valid:
|
| 62 |
+
logging.error(f"Failed to load {file.name}: {error_msg}")
|
| 63 |
+
return None, None, None, f"Error loading {file.name}: {error_msg}"
|
| 64 |
all_data.append(df)
|
| 65 |
except Exception as e:
|
| 66 |
logging.error(f"Failed to load {file.name}: {str(e)}")
|
|
|
|
| 101 |
# Prepare output dataframe (combine original data with anomalies)
|
| 102 |
output_df = combined_df.copy()
|
| 103 |
if anomaly_df is not None:
|
| 104 |
+
output_df['anomaly'] = anomaly_df['anomaly'].map({1: "Normal", -1: "Anomaly"})
|
| 105 |
|
| 106 |
return output_df, plot_path, pdf_path, amc_message
|
| 107 |
|
|
|
|
| 111 |
Returns the path to the saved plot.
|
| 112 |
"""
|
| 113 |
try:
|
| 114 |
+
plt.figure(figsize=(12, 6))
|
| 115 |
+
# Define colors for statuses
|
| 116 |
+
status_colors = {'Active': '#36A2EB', 'Inactive': '#FF6384', 'Down': '#FFCE56', 'Online': '#4BC0C0'}
|
| 117 |
for status in df['status'].unique():
|
| 118 |
subset = df[df['status'] == status]
|
| 119 |
+
plt.bar(
|
| 120 |
+
subset['equipment'] + f" ({status})",
|
| 121 |
+
subset['usage_count'],
|
| 122 |
+
label=status,
|
| 123 |
+
color=status_colors.get(status, '#999999')
|
| 124 |
+
)
|
| 125 |
+
plt.xlabel("Equipment (Status)", fontsize=12)
|
| 126 |
+
plt.ylabel("Usage Count", fontsize=12)
|
| 127 |
+
plt.title("Usage Count by Equipment and Status", fontsize=14)
|
| 128 |
+
plt.legend(title="Status")
|
| 129 |
+
plt.xticks(rotation=45, ha='right')
|
| 130 |
plt.tight_layout()
|
| 131 |
|
| 132 |
# Save plot to temporary file
|
| 133 |
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp:
|
| 134 |
+
plt.savefig(tmp.name, format='png', dpi=100)
|
| 135 |
plot_path = tmp.name
|
| 136 |
plt.close()
|
| 137 |
return plot_path
|
|
|
|
| 165 |
df['amc_expiry'] = pd.to_datetime(df['amc_expiry'])
|
| 166 |
upcoming_expiries = df[df['amc_expiry'] <= threshold]
|
| 167 |
unique_devices = upcoming_expiries['equipment'].unique()
|
| 168 |
+
message = f"Found {len(unique_devices)} devices with upcoming AMC expiries: {', '.join(unique_devices)}."
|
| 169 |
+
logging.info(f"Found {len(unique_devices)} devices with upcoming AMC expiries.")
|
| 170 |
return message, upcoming_expiries
|
| 171 |
except Exception as e:
|
| 172 |
logging.error(f"Failed to process AMC expiries: {str(e)}")
|
|
|
|
| 178 |
Returns the path to the saved PDF.
|
| 179 |
"""
|
| 180 |
try:
|
| 181 |
+
if original_df is None or original_df.empty:
|
| 182 |
logging.warning("No data available for PDF generation.")
|
| 183 |
return None
|
| 184 |
|
| 185 |
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp:
|
| 186 |
c = canvas.Canvas(tmp.name, pagesize=letter)
|
| 187 |
+
c.setFont("Helvetica-Bold", 16)
|
| 188 |
c.drawString(100, 750, "Equipment Log Analysis Report")
|
| 189 |
+
c.setFont("Helvetica", 12)
|
| 190 |
+
y = 720
|
| 191 |
|
| 192 |
# Summary
|
| 193 |
+
c.drawString(100, y, "Summary")
|
| 194 |
+
y -= 20
|
| 195 |
c.drawString(100, y, f"Total Records: {len(original_df)}")
|
| 196 |
+
y -= 20
|
| 197 |
+
c.drawString(100, y, f"Devices: {', '.join(original_df['equipment'].unique())}")
|
| 198 |
y -= 40
|
| 199 |
|
| 200 |
# Anomalies
|
| 201 |
+
c.drawString(100, y, "Anomaly Detection Results")
|
| 202 |
+
y -= 20
|
| 203 |
if anomaly_df is not None:
|
| 204 |
num_anomalies = sum(anomaly_df['anomaly'] == -1)
|
| 205 |
c.drawString(100, y, f"Anomalies Detected: {num_anomalies}")
|
| 206 |
+
y -= 20
|
| 207 |
if num_anomalies > 0:
|
| 208 |
+
anomaly_records = anomaly_df[anomaly_df['anomaly'] == -1][['equipment', 'usage_count']]
|
| 209 |
+
c.drawString(100, y, "Anomalous Records:")
|
| 210 |
+
y -= 20
|
| 211 |
+
for _, row in anomaly_records.iterrows():
|
| 212 |
+
c.drawString(100, y, f"{row['equipment']}: Usage Count = {row['usage_count']}")
|
| 213 |
+
y -= 20
|
| 214 |
+
if y < 50:
|
| 215 |
+
c.showPage()
|
| 216 |
+
y = 750
|
| 217 |
else:
|
| 218 |
c.drawString(100, y, "Anomaly detection failed.")
|
| 219 |
y -= 20
|
| 220 |
+
y -= 20
|
| 221 |
|
| 222 |
# AMC Expiries
|
| 223 |
+
c.drawString(100, y, "AMC Expiries Within 7 Days")
|
| 224 |
+
y -= 20
|
| 225 |
if amc_df is not None and not amc_df.empty:
|
| 226 |
c.drawString(100, y, f"Devices with Upcoming AMC Expiries: {len(amc_df['equipment'].unique())}")
|
| 227 |
+
y -= 20
|
| 228 |
for _, row in amc_df.iterrows():
|
| 229 |
+
c.drawString(100, y, f"{row['equipment']}: {row['amc_expiry'].strftime('%Y-%m-%d')}")
|
| 230 |
y -= 20
|
| 231 |
+
if y < 50:
|
| 232 |
+
c.showPage()
|
| 233 |
+
y = 750
|
| 234 |
else:
|
| 235 |
c.drawString(100, y, "No AMC expiry data available.")
|
| 236 |
y -= 20
|