218 lines
8.6 KiB
Python
Executable file
218 lines
8.6 KiB
Python
Executable file
import os
|
|
import json
|
|
import sys
|
|
from datetime import datetime
|
|
|
|
# Add parent directory to path for config import
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
import config
|
|
|
|
class HTMLReporter:
|
|
@staticmethod
|
|
def generate_report(json_data: dict, reports_dir: str, input_filename: str) -> str:
|
|
"""
|
|
Updated method signature with proper parameters
|
|
"""
|
|
try:
|
|
# Create directory if needed
|
|
os.makedirs(reports_dir, exist_ok=True)
|
|
|
|
# Generate filename
|
|
date_str = datetime.now().isoformat(timespec='seconds').replace(":", "-")
|
|
safe_name = (input_filename.rpartition('.')[0].replace('.', '_') + ('.' + input_filename.rpartition('.')[2] if input_filename.rpartition('.')[1] else input_filename)).replace(" ", "_").replace(":", "-").split('.')[0]
|
|
report_filename = f"{safe_name}_{date_str}_QC.html"
|
|
output_path = os.path.join(reports_dir, report_filename)
|
|
|
|
# Generate HTML content
|
|
html_content = HTMLReporter._build_html_template(json_data, input_filename)
|
|
|
|
# Write to file
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
# Extract campaign path and create QC directory
|
|
# Pattern: /data/BOXSYNC/HM_AME/HM/CAMPAIGNS/{campaign_id}/JOBS/{job_id}/LOGS
|
|
path_parts = reports_dir.split('/')
|
|
if 'CAMPAIGNS' in path_parts:
|
|
campaign_index = path_parts.index('CAMPAIGNS')
|
|
if len(path_parts) > campaign_index + 1:
|
|
campaign_id = path_parts[campaign_index + 1]
|
|
# Create path to QC folder in the campaign directory
|
|
qc_dir = '/'.join(path_parts[:campaign_index + 2]) + '/QC/'
|
|
|
|
# Create QC directory if it doesn't exist
|
|
os.makedirs(qc_dir, exist_ok=True)
|
|
|
|
# Full path for the QC report
|
|
qc_report_path = os.path.join(qc_dir, report_filename)
|
|
|
|
# Write to QC directory
|
|
with open(qc_report_path, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
# Write to environment-aware generic reports dir (production: /opt/QC/reports/, dev: ./tmp/reports/)
|
|
os.makedirs(config.REPORTS_DIR, exist_ok=True)
|
|
report_path = os.path.join(config.REPORTS_DIR, report_filename)
|
|
with open(report_path, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
return output_path
|
|
|
|
except Exception as e:
|
|
raise RuntimeError(f"HTML report generation failed: {str(e)}") from e
|
|
|
|
|
|
@staticmethod
|
|
def _build_html_template(json_data: dict, input_filename: str) -> str:
|
|
"""Construct the full HTML template with data"""
|
|
return f'''
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
{HTMLReporter._build_head(input_filename, json_data['timestamp'])}
|
|
<body>
|
|
<div class="container py-4">
|
|
{HTMLReporter._build_header(input_filename, json_data['timestamp'])}
|
|
<div class="accordion" id="checksAccordion">
|
|
{''.join(HTMLReporter._generate_check_html(check) for check in json_data['checks'])}
|
|
</div>
|
|
</div>
|
|
{HTMLReporter._build_scripts()}
|
|
</body>
|
|
</html>
|
|
'''
|
|
|
|
@staticmethod
|
|
def _build_head(filename: str, timestamp: str) -> str:
|
|
"""Build the HTML head section"""
|
|
return f'''
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>QC Report - {filename}</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<style>
|
|
{HTMLReporter._get_css_styles()}
|
|
</style>
|
|
</head>
|
|
'''
|
|
|
|
@staticmethod
|
|
def _get_css_styles() -> str:
|
|
"""Return CSS styles"""
|
|
return '''
|
|
.status-badge { font-size: 0.8rem; padding: 0.35em 0.65em; }
|
|
.check-card { margin-bottom: 1rem; }
|
|
.details-list { list-style-type: none; padding-left: 1.5rem; }
|
|
.details-list li { margin-bottom: 0.5rem; }
|
|
.nested-details { padding-left: 1.5rem; margin-top: 0.5rem; border-left: 2px solid #dee2e6; }
|
|
.error-section { background-color: #fff3cd; border-radius: 4px; padding: 1rem; margin: 1rem 0; }
|
|
'''
|
|
|
|
@staticmethod
|
|
def _build_header(filename: str, timestamp: str) -> str:
|
|
"""Build the header section"""
|
|
return f'''
|
|
<header class="mb-4">
|
|
<h1 class="display-4">QC Report: {filename}</h1>
|
|
<p class="text-muted">Generated at: {datetime.fromisoformat(timestamp.rstrip('Z')).strftime('%Y-%m-%d %H:%M:%S')}</p>
|
|
</header>
|
|
'''
|
|
|
|
@staticmethod
|
|
def _build_scripts() -> str:
|
|
"""Include required JavaScript"""
|
|
return '''
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
'''
|
|
|
|
@staticmethod
|
|
def _generate_check_html(check: dict) -> str:
|
|
"""Generate HTML for an individual check"""
|
|
status_color = {
|
|
'passed': 'success',
|
|
'error': 'danger',
|
|
'failed': 'warning',
|
|
'skipped': 'secondary'
|
|
}.get(check['result']['status'].lower(), 'secondary')
|
|
|
|
return f'''
|
|
<div class="accordion-item check-card">
|
|
<h2 class="accordion-header" id="heading{check['index']}">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
|
data-bs-target="#collapse{check['index']}" aria-expanded="false"
|
|
aria-controls="collapse{check['index']}">
|
|
<span class="badge bg-{status_color} status-badge me-2">{check['result']['status'].upper()}</span>
|
|
{check['id']}: {check['config'].get('description', 'No description')}
|
|
</button>
|
|
</h2>
|
|
<div id="collapse{check['index']}" class="accordion-collapse collapse"
|
|
aria-labelledby="heading{check['index']}" data-bs-parent="#checksAccordion">
|
|
<div class="accordion-body">
|
|
{HTMLReporter._generate_error_section(check['result'])}
|
|
<h5>Configuration</h5>
|
|
<ul class="details-list">
|
|
{HTMLReporter._format_details(check['config'])}
|
|
</ul>
|
|
<h5>Results</h5>
|
|
<ul class="details-list">
|
|
{HTMLReporter._format_details(check['result'].get('details', {}))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
'''
|
|
|
|
@staticmethod
|
|
def _generate_error_section(result: dict) -> str:
|
|
"""Generate error section if present"""
|
|
if 'error_message' not in result:
|
|
return ''
|
|
|
|
return f'''
|
|
<div class="error-section">
|
|
<h5 class="text-danger">Error:</h5>
|
|
<p>{result['error_message']}</p>
|
|
{HTMLReporter._format_details(result.get('details', {}))}
|
|
</div>
|
|
'''
|
|
|
|
@staticmethod
|
|
def _format_details(details: dict, level: int = 0) -> str:
|
|
"""Recursively format nested details"""
|
|
items = []
|
|
for key, value in details.items():
|
|
if key == 'error_message':
|
|
continue
|
|
|
|
if isinstance(value, dict):
|
|
items.append(f'''
|
|
<li>
|
|
<strong>{key.title()}:</strong>
|
|
<div class="nested-details">
|
|
{HTMLReporter._format_details(value, level+1)}
|
|
</div>
|
|
</li>
|
|
''')
|
|
elif isinstance(value, list):
|
|
list_items = ''.join(f'<li>{item}</li>' for item in value)
|
|
items.append(f'''
|
|
<li>
|
|
<strong>{key.title()}:</strong>
|
|
<ul>{list_items}</ul>
|
|
</li>
|
|
''')
|
|
else:
|
|
items.append(f'<li><strong>{key.title()}:</strong> {value}</li>')
|
|
return '\n'.join(items)
|
|
|
|
if __name__ == "__main__":
|
|
# Example usage
|
|
import sys
|
|
if len(sys.argv) != 3:
|
|
print("Usage: python html_reporter.py <input_json> <output_html>")
|
|
sys.exit(1)
|
|
|
|
with open(sys.argv[1]) as f:
|
|
data = json.load(f)
|
|
|
|
HTMLReporter.generate_report(data, sys.argv[2])
|