-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi.py
More file actions
599 lines (497 loc) · 20.6 KB
/
Copy pathapi.py
File metadata and controls
599 lines (497 loc) · 20.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
#!/usr/bin/env python3
"""
TAC Executive Report Generator API
Flask-based REST API for generating TAC case executive reports.
Supports CSV and Excel file uploads with HTML/PDF report generation.
PURPOSE:
This API provides a web service interface to the TAC analyzer, allowing:
- Remote file uploads and analysis
- Integration with other systems via HTTP
- Cloud deployment and scalability
- Multiple concurrent report generation
USAGE:
- Can be deployed as a microservice using Docker/Kubernetes
- Alternative to command-line analyzer for web applications
- Enables browser-based TAC report generation
- Supports REST API integration with CI/CD pipelines
Endpoints:
POST /api/tac/analyze - Upload file and generate report
POST /api/tac/analyze-batch - Upload multiple files and generate batch report
GET /api/tac/health - Health check endpoint
GET / - API documentation
"""
import os
import json
import traceback
import tempfile
import shutil
import uuid
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, Any, List, Optional
from flask import Flask, request, jsonify, send_file, render_template_string
from werkzeug.utils import secure_filename
import logging
from threading import Lock
# Import the TAC analyzer modules
from tac_analyzer import TACAnalyzer
from tac_data_processor import TACDataProcessor
from tac_report_generator import TACReportGenerator
from tac_config import OUTPUT_FORMATS
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB max file size
app.config['UPLOAD_FOLDER'] = tempfile.gettempdir()
# Create a persistent reports directory
# Use environment variable or default to ./reports for flexibility
REPORTS_DIR = Path(os.getenv('REPORTS_DIR', 'reports'))
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
# Allowed file extensions
ALLOWED_EXTENSIONS = {'csv', 'xlsx', 'xls'}
# In-memory cache for report metadata (report_id -> file_path mapping)
# In production, use Redis or a database
report_cache = {}
cache_lock = Lock()
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def allowed_file(filename: str) -> bool:
"""Check if the uploaded file has an allowed extension."""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def perform_analysis(file_path: Path, output_formats: Optional[List[str]] = None) -> Dict[str, Any]:
"""
Perform TAC case analysis on a single file.
Args:
file_path: Path to the input file
output_formats: List of output formats ['html', 'pdf']
Returns:
dict: Result containing success status, report data, and any errors
"""
if output_formats is None:
output_formats = ['html']
result = {
'success': False,
'error': None,
'report_data': None,
'stats': {},
'generated_files': {},
'report_ids': {}
}
try:
# Use persistent reports directory instead of temp
output_dir = REPORTS_DIR
logger.info(f"Processing file: {file_path}")
# Process the data
processor = TACDataProcessor(file_path)
file_analysis = processor.load_and_analyze()
if file_analysis['total_cases'] == 0:
result['error'] = "No valid TAC cases found in the file"
return result
# Process executive analytics (instead of compute_insights)
analytics = processor.process_executive_analytics()
# Generate reports using the correct method
report_generator = TACReportGenerator(output_dir)
generated_files = report_generator.generate_reports(
input_filename=file_path.name,
analytics=analytics,
file_analysis=file_analysis,
formats=output_formats
)
# Store generated file paths and create report IDs
with cache_lock:
for fmt, report_path in generated_files.items():
if report_path and report_path.exists():
# Generate unique report ID
report_id = str(uuid.uuid4())
# Store in cache with metadata
report_cache[report_id] = {
'path': str(report_path),
'format': fmt,
'filename': report_path.name,
'created_at': datetime.now(),
'expires_at': datetime.now() + timedelta(hours=24) # 24-hour expiry
}
result['generated_files'][fmt] = str(report_path)
result['report_ids'][fmt] = report_id
logger.info(f"Generated {fmt.upper()} report: {report_path} (ID: {report_id})")
# Prepare result data
result['success'] = len(result['generated_files']) > 0
result['stats'] = {
'total_cases': file_analysis['total_cases'],
'date_range': file_analysis.get('date_range', {}),
'columns_found': file_analysis.get('columns_found', 0),
'analytics_sections': len(analytics)
}
result['report_data'] = {
'file_name': file_path.name,
'processed_at': datetime.now().isoformat(),
'analytics_summary': {
'total_cases': analytics.get('summary', {}).get('total_cases', 0),
'severity_levels': len(analytics.get('severity_analysis', {}).get('by_severity', {})),
'products_analyzed': len(analytics.get('product_analysis', {}).get('top_products', [])),
'engineers_assigned': len(analytics.get('engineer_assignment', {}).get('top_engineers', []))
}
}
except Exception as e:
result['error'] = str(e)
result['traceback'] = traceback.format_exc()
logger.error(f"Analysis failed: {e}")
logger.error(traceback.format_exc())
return result
@app.route('/')
def index():
"""Display API documentation."""
html_doc = """
<!DOCTYPE html>
<html>
<head>
<title>TAC Report Generator API</title>
<style>
body { font-family: Arial, sans-serif; max-width: 1200px; margin: 50px auto; padding: 20px; }
h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
h2 { color: #34495e; margin-top: 30px; }
.endpoint { background: #ecf0f1; padding: 15px; margin: 15px 0; border-radius: 5px; }
.method { color: #27ae60; font-weight: bold; }
.path { color: #2980b9; font-family: monospace; }
code { background: #2c3e50; color: #ecf0f1; padding: 2px 6px; border-radius: 3px; }
pre { background: #2c3e50; color: #ecf0f1; padding: 15px; border-radius: 5px; overflow-x: auto; }
</style>
</head>
<body>
<h1>📊 TAC Executive Report Generator API</h1>
<p>RESTful API for generating professional TAC (Technical Assistance Center) case reports with interactive visualizations.</p>
<h2>Available Endpoints</h2>
<div class="endpoint">
<h3><span class="method">POST</span> <span class="path">/api/tac/analyze</span></h3>
<p>Upload a single TAC case file (CSV or Excel) and generate an executive report.</p>
<p><strong>Request:</strong> multipart/form-data</p>
<ul>
<li><code>file</code> - CSV or Excel file containing TAC cases</li>
<li><code>format</code> (optional) - Output format: "html", "pdf", or "both" (default: "html")</li>
</ul>
<p><strong>Response:</strong> JSON with report metadata and download URLs</p>
<p><strong>Note:</strong> Use the <code>reports</code> URLs to download the generated files. Reports are cached for 24 hours.</p>
</div>
<div class="endpoint">
<h3><span class="method">POST</span> <span class="path">/api/tac/analyze-batch</span></h3>
<p>Upload multiple TAC case files and generate individual reports plus a batch summary.</p>
<p><strong>Request:</strong> multipart/form-data</p>
<ul>
<li><code>files</code> - Multiple CSV or Excel files</li>
<li><code>format</code> (optional) - Output format: "html", "pdf", or "both" (default: "html")</li>
</ul>
<p><strong>Response:</strong> JSON with batch processing results</p>
</div>
<div class="endpoint">
<h3><span class="method">GET</span> <span class="path">/api/tac/report/{report_id}</span></h3>
<p>Download a generated report file using its unique ID.</p>
<p><strong>Parameters:</strong></p>
<ul>
<li><code>report_id</code> - UUID returned from analyze endpoint</li>
</ul>
<p><strong>Response:</strong> Report file (HTML or PDF) for download</p>
<p><strong>Note:</strong> Reports expire after 24 hours.</p>
</div>
<div class="endpoint">
<h3><span class="method">GET</span> <span class="path">/api/tac/health</span></h3>
<p>Health check endpoint for monitoring.</p>
<p><strong>Response:</strong> JSON with service status and cached report count</p>
</div>
<div class="endpoint">
<h3><span class="method">POST</span> <span class="path">/api/tac/reports/cleanup</span></h3>
<p>Clean up expired reports from cache (admin endpoint).</p>
<p><strong>Response:</strong> JSON with cleanup results</p>
</div>
<h2>Example Usage</h2>
<pre>
# Upload and analyze a single file
curl -X POST http://localhost:5000/api/tac/analyze \\
-F "file=@tac_cases.csv" \\
-F "format=html"
# Response will include:
# {
# "reports": {
# "html": "/api/tac/report/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# }
# }
# Download the report using the URL
curl -O -J http://localhost:5000/api/tac/report/a1b2c3d4-e5f6-7890-abcd-ef1234567890
# Upload multiple files for batch processing
curl -X POST http://localhost:5000/api/tac/analyze-batch \\
-F "files=@tac_q1.csv" \\
-F "files=@tac_q2.csv" \\
-F "format=both"
# Check service health
curl http://localhost:5000/api/tac/health
# Clean up expired reports
curl -X POST http://localhost:5000/api/tac/reports/cleanup
</pre>
<h2>Supported File Formats</h2>
<ul>
<li>CSV (.csv)</li>
<li>Excel (.xlsx, .xls)</li>
</ul>
<h2>Report Formats</h2>
<ul>
<li><strong>HTML</strong> - Interactive report with Plotly charts</li>
<li><strong>PDF</strong> - Static report for sharing (requires Playwright)</li>
</ul>
</body>
</html>
"""
return render_template_string(html_doc)
@app.route('/api/tac/health', methods=['GET'])
def health_check():
"""Health check endpoint."""
with cache_lock:
cached_reports = len(report_cache)
return jsonify({
'status': 'healthy',
'service': 'tac-report-generator',
'version': '1.0.0',
'timestamp': datetime.now().isoformat(),
'cached_reports': cached_reports
})
@app.route('/api/tac/reports/cleanup', methods=['POST'])
def cleanup_expired_reports():
"""
Clean up expired reports from cache and disk.
Can be called periodically by a scheduler.
"""
try:
cleaned_count = 0
with cache_lock:
expired_ids = []
for report_id, report_info in report_cache.items():
if datetime.now() > report_info['expires_at']:
expired_ids.append(report_id)
for report_id in expired_ids:
report_info = report_cache[report_id]
report_path = Path(report_info['path'])
# Delete file if exists
if report_path.exists():
try:
report_path.unlink()
logger.info(f"Deleted expired report: {report_path}")
except Exception as e:
logger.warning(f"Failed to delete {report_path}: {e}")
# Remove from cache
del report_cache[report_id]
cleaned_count += 1
return jsonify({
'success': True,
'message': f'Cleaned up {cleaned_count} expired reports',
'cleaned_count': cleaned_count
}), 200
except Exception as e:
logger.error(f"Cleanup error: {e}")
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/tac/analyze', methods=['POST'])
def analyze_file():
"""
Analyze a single TAC case file and generate report.
Expects:
- file: CSV or Excel file (multipart/form-data)
- format: Optional output format (html, pdf, both)
"""
try:
# Validate file presence
if 'file' not in request.files:
return jsonify({
'success': False,
'error': 'No file provided'
}), 400
file = request.files['file']
if not file.filename or file.filename == '':
return jsonify({
'success': False,
'error': 'Empty filename'
}), 400
if not allowed_file(file.filename):
return jsonify({
'success': False,
'error': f'Invalid file type. Allowed: {", ".join(ALLOWED_EXTENSIONS)}'
}), 400
# Get output format
output_format = request.form.get('format', 'html').lower()
if output_format == 'both':
output_formats = ['html', 'pdf']
else:
output_formats = [output_format]
# Save uploaded file
filename = secure_filename(file.filename)
temp_dir = tempfile.mkdtemp()
file_path = Path(temp_dir) / filename
file.save(str(file_path))
logger.info(f"Received file: {filename} ({file_path.stat().st_size} bytes)")
# Perform analysis
result = perform_analysis(file_path, output_formats)
# Clean up uploaded file
if file_path.exists():
file_path.unlink()
if result['success']:
# Store report files temporarily (in production, use cloud storage)
report_links = {}
for fmt, report_id in result['report_ids'].items():
report_links[fmt] = f"/api/tac/report/{report_id}"
return jsonify({
'success': True,
'message': 'Analysis completed successfully',
'data': result['report_data'],
'stats': result['stats'],
'reports': report_links, # Use these URLs to download reports
'report_ids': result['report_ids'] # Keep IDs for reference
}), 200
else:
return jsonify({
'success': False,
'error': result.get('error', 'Analysis failed'),
'details': result.get('traceback')
}), 500
except Exception as e:
logger.error(f"API error: {e}")
logger.error(traceback.format_exc())
return jsonify({
'success': False,
'error': str(e),
'traceback': traceback.format_exc()
}), 500
@app.route('/api/tac/analyze-batch', methods=['POST'])
def analyze_batch():
"""
Analyze multiple TAC case files and generate batch report.
Expects:
- files: Multiple CSV or Excel files (multipart/form-data)
- format: Optional output format (html, pdf, both)
"""
try:
# Validate files presence
if 'files' not in request.files:
return jsonify({
'success': False,
'error': 'No files provided'
}), 400
files = request.files.getlist('files')
if len(files) == 0:
return jsonify({
'success': False,
'error': 'No files provided'
}), 400
# Get output format
output_format = request.form.get('format', 'html').lower()
if output_format == 'both':
output_formats = ['html', 'pdf']
else:
output_formats = [output_format]
# Process each file
results = []
temp_dir = tempfile.mkdtemp()
for file in files:
if not file.filename or file.filename == '' or not allowed_file(file.filename):
continue
filename = secure_filename(file.filename)
file_path = Path(temp_dir) / filename
file.save(str(file_path))
logger.info(f"Processing batch file: {filename}")
result = perform_analysis(file_path, output_formats)
result['filename'] = filename
results.append(result)
# Calculate batch statistics
successful = sum(1 for r in results if r['success'])
failed = len(results) - successful
return jsonify({
'success': successful > 0,
'message': f'Batch processing completed',
'total_files': len(results),
'successful': successful,
'failed': failed,
'results': results
}), 200
except Exception as e:
logger.error(f"Batch API error: {e}")
logger.error(traceback.format_exc())
return jsonify({
'success': False,
'error': str(e),
'traceback': traceback.format_exc()
}), 500
@app.route('/api/tac/report/<report_id>', methods=['GET'])
def download_report(report_id):
"""
Download a generated report file.
Args:
report_id: Unique report identifier (UUID)
"""
try:
# Look up report in cache
with cache_lock:
if report_id not in report_cache:
return jsonify({
'success': False,
'error': 'Report not found or has expired'
}), 404
report_info = report_cache[report_id]
# Check if expired
if datetime.now() > report_info['expires_at']:
# Clean up expired report
report_path = Path(report_info['path'])
if report_path.exists():
report_path.unlink()
del report_cache[report_id]
return jsonify({
'success': False,
'error': 'Report has expired'
}), 410 # Gone
report_path = Path(report_info['path'])
# Verify file exists
if not report_path.exists():
del report_cache[report_id]
return jsonify({
'success': False,
'error': 'Report file not found on disk'
}), 404
# Determine MIME type
mime_type = 'text/html' if report_info['format'] == 'html' else 'application/pdf'
# Send file
return send_file(
report_path,
mimetype=mime_type,
as_attachment=True,
download_name=report_info['filename']
)
except Exception as e:
logger.error(f"Download error: {e}")
logger.error(traceback.format_exc())
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.errorhandler(413)
def request_entity_too_large(error):
"""Handle file too large error."""
return jsonify({
'success': False,
'error': 'File too large. Maximum size: 50MB'
}), 413
@app.errorhandler(500)
def internal_error(error):
"""Handle internal server errors."""
logger.error(f"Internal error: {error}")
return jsonify({
'success': False,
'error': 'Internal server error'
}), 500
if __name__ == '__main__':
# Create necessary directories
os.makedirs('input_data', exist_ok=True)
os.makedirs('reports', exist_ok=True)
os.makedirs('temp', exist_ok=True)
# Run the Flask app
logger.info("Starting TAC Report Generator API...")
app.run(host='0.0.0.0', port=5000, debug=os.getenv('FLASK_DEBUG', 'False').lower() == 'true')