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
|