hm_qc/utils/report.py
2025-09-30 10:37:12 -05:00

140 lines
No EOL
4.7 KiB
Python
Executable file

import json
from datetime import datetime
def generate_html_report(json_data, output_file):
# Extract input filename from first check
input_file_path = json_data['checks'][0]['config']['input_file']
input_filename = input_file_path.split('/')[-1] # Get just the filename
# HTML template with Bootstrap for styling
html_template = f'''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QC Report - {input_filename}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.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;
}}
</style>
</head>
<body>
<div class="container py-4">
<header class="mb-4">
<h1 class="display-4">QC Report: {input_filename}</h1>
<p class="text-muted">Generated at: {datetime.fromisoformat(json_data["timestamp"]).strftime('%Y-%m-%d %H:%M:%S')}</p>
</header>
<div class="accordion" id="checksAccordion">
{''.join([generate_check_html(check) for check in json_data['checks']])}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
'''
with open(output_file, 'w') as f:
f.write(html_template)
def generate_check_html(check):
status_color = {
'passed': 'success',
'error': 'danger',
'failed': 'warning'
}.get(check['result']['status'].lower(), 'secondary')
details_html = format_details(check['result'].get('details', {}))
error_html = ''
if 'error_message' in check['result']:
error_html = f'''
<div class="error-section">
<h5 class="text-danger">Error:</h5>
<p>{check['result']['error_message']}</p>
</div>
'''
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']['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">
{error_html}
<h5>Configuration</h5>
<ul class="details-list">
{format_details(check['config'])}
</ul>
<h5>Results</h5>
<ul class="details-list">
{details_html}
</ul>
</div>
</div>
</div>
'''
def format_details(details, level=0):
items = []
for key, value in details.items():
if isinstance(value, dict):
items.append(f'''
<li>
<strong>{key.title()}:</strong>
<div class="nested-details">
{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)
# Example usage
if __name__ == "__main__":
with open('input_report.json') as f:
data = json.load(f)
generate_html_report(data, 'qc_report.html')