Skip to content

Instantly share code, notes, and snippets.

@pjaudiomv
Created January 2, 2026 16:23
Show Gist options
  • Select an option

  • Save pjaudiomv/a1e87fff10e165a2f5e98c5cd9af7d59 to your computer and use it in GitHub Desktop.

Select an option

Save pjaudiomv/a1e87fff10e165a2f5e98c5cd9af7d59 to your computer and use it in GitHub Desktop.
Generate a PDF report from audit_duplicate_keys_results.json
#!/usr/bin/env python3
"""
Generate a PDF report from audit_duplicate_keys_results.json
This script reads the JSON output from audit_duplicate_format_keys.py
and generates a formatted PDF report.
Usage:
python3 generate_pdf_report.py # Use default input file
python3 generate_pdf_report.py audit_duplicate_keys_results.json
python3 generate_pdf_report.py input.json output.pdf
Requirements:
pip install reportlab
"""
import json
import sys
from datetime import datetime
from typing import List, Dict
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter, A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak
from reportlab.platypus import KeepTogether
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT
def load_results(filepath: str) -> List[Dict]:
"""Load audit results from JSON file."""
try:
with open(filepath, 'r') as f:
return json.load(f)
except FileNotFoundError:
print(f"❌ Error: File '{filepath}' not found", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"❌ Error parsing JSON: {e}", file=sys.stderr)
sys.exit(1)
def create_pdf_report(results: List[Dict], output_file: str):
"""Generate a PDF report from audit results."""
doc = SimpleDocTemplate(
output_file,
pagesize=letter,
rightMargin=0.5*inch,
leftMargin=0.5*inch,
topMargin=0.75*inch,
bottomMargin=0.75*inch
)
# Container for the 'Flowable' objects
elements = []
# Define styles
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
'CustomTitle',
parent=styles['Heading1'],
fontSize=24,
textColor=colors.HexColor('#2c3e50'),
spaceAfter=30,
alignment=TA_CENTER
)
heading_style = ParagraphStyle(
'CustomHeading',
parent=styles['Heading2'],
fontSize=16,
textColor=colors.HexColor('#34495e'),
spaceAfter=12,
spaceBefore=12
)
subheading_style = ParagraphStyle(
'CustomSubHeading',
parent=styles['Heading3'],
fontSize=12,
textColor=colors.HexColor('#7f8c8d'),
spaceAfter=8,
spaceBefore=8
)
normal_style = styles['Normal']
# Title
title = Paragraph("BMLT Duplicate Format Keys Audit Report", title_style)
elements.append(title)
# Report metadata
report_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
metadata = Paragraph(f"<i>Generated: {report_date}</i>", normal_style)
elements.append(metadata)
elements.append(Spacer(1, 0.3*inch))
# Summary statistics
total_servers = len(results)
servers_with_duplicates = sum(1 for r in results if r.get('duplicates'))
total_duplicate_groups = sum(len(r.get('duplicates', [])) for r in results)
summary_heading = Paragraph("Summary", heading_style)
elements.append(summary_heading)
summary_data = [
['Metric', 'Value'],
['Total Servers Audited', str(total_servers)],
['Servers with Duplicate Keys', str(servers_with_duplicates)],
['Total Duplicate Key Groups', str(total_duplicate_groups)]
]
summary_table = Table(summary_data, colWidths=[3.5*inch, 2*inch])
summary_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3498db')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
('GRID', (0, 0), (-1, -1), 1, colors.grey),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 10),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#ecf0f1')])
]))
elements.append(summary_table)
elements.append(Spacer(1, 0.4*inch))
# Recommendations section
if total_duplicate_groups > 0:
recommendations_heading = Paragraph("How to Fix Duplicate Format Keys", heading_style)
elements.append(recommendations_heading)
recommendations_text = """
<b>Why This Is a Problem:</b><br/>
Duplicate format keys can cause confusion for users and client applications. When multiple formats
share the same key_string in the same language, it becomes unclear which format is being referenced.
This can lead to inconsistent meeting search results and display issues.<br/><br/>
<b>Recommended Solutions:</b><br/>
<br/>
<b>1. Merge Duplicate Formats</b><br/>
If the duplicate formats represent the same concept, merge them into a single format:
<br/>
• Choose one format to keep (usually the one used by more meetings)<br/>
• Update all meetings using the other format(s) to use the kept format<br/>
• Delete the redundant format(s)<br/>
<br/>
<b>2. Rename Format Keys</b><br/>
If the formats represent different concepts, give them unique keys:
<br/>
• Review what each format represents<br/>
• Assign a unique, descriptive key to each format<br/>
• Keep the key short (1-3 characters) for compatibility<br/>
<br/>
<b>3. Use Database Updates</b><br/>
For formats with many meetings, use SQL to bulk update:<br/>
<br/>
<font face="Courier" size="8">
-- Example: Merge format 36 into format 29<br/>
UPDATE na_comdef_meetings_main SET format_shared_id_list = <br/>
&nbsp;&nbsp;REPLACE(format_shared_id_list, ',36,', ',29,');<br/>
DELETE FROM na_comdef_formats WHERE shared_id_bigint = 36;<br/>
</font>
<br/>
<b>4. Test After Changes</b><br/>
After making changes:<br/>
• Clear any application caches<br/>
• Verify meeting searches work correctly<br/>
• Check that format displays are consistent<br/>
• Re-run this audit to confirm duplicates are resolved<br/>
"""
recommendations_para = Paragraph(recommendations_text, normal_style)
elements.append(recommendations_para)
elements.append(Spacer(1, 0.4*inch))
# Detailed results
if total_duplicate_groups > 0:
details_heading = Paragraph("Detailed Results", heading_style)
elements.append(details_heading)
elements.append(Spacer(1, 0.2*inch))
for result in results:
if not result.get('duplicates'):
continue
server_name = result.get('server', 'Unknown Server')
server_id = result.get('server_id', 'N/A')
server_url = result.get('url', 'N/A')
# Server header
server_info = f"<b>{server_name}</b> (ID: {server_id})"
server_para = Paragraph(server_info, subheading_style)
url_para = Paragraph(f"<i>{server_url}</i>", normal_style)
server_elements = [server_para, url_para, Spacer(1, 0.1*inch)]
# Process each duplicate group
for dup in result['duplicates']:
lang = dup.get('language', 'unknown')
key = dup.get('key', 'unknown')
formats = dup.get('formats', [])
dup_header = Paragraph(
f"<b>Language:</b> {lang} | <b>Key:</b> {key} | <b>Count:</b> {len(formats)}",
normal_style
)
server_elements.append(dup_header)
server_elements.append(Spacer(1, 0.1*inch))
# Format details table
format_data = [['Format ID', 'Name', 'Meetings Using This Format']]
for fmt in formats:
fmt_id = fmt.get('id', 'N/A')
fmt_name = fmt.get('name_string', 'N/A')
# Get meeting IDs for this format
format_usage = result.get('format_usage', {})
meeting_ids = format_usage.get(fmt_id, [])
if meeting_ids:
# Sort and format meeting IDs
sorted_meetings = sorted(meeting_ids)
meeting_str = ', '.join(str(mid) for mid in sorted_meetings)
# Wrap in Paragraph to enable text wrapping
meeting_count = Paragraph(
f"<b>{len(meeting_ids)} meeting(s):</b><br/>{meeting_str}",
normal_style
)
else:
meeting_count = Paragraph("No meetings found", normal_style)
# Wrap other cells in Paragraph too for consistency
format_data.append([
Paragraph(str(fmt_id), normal_style),
Paragraph(str(fmt_name), normal_style),
meeting_count
])
format_table = Table(format_data, colWidths=[0.8*inch, 2.2*inch, 4*inch])
format_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#e74c3c')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 10),
('BOTTOMPADDING', (0, 0), (-1, 0), 8),
('TOPPADDING', (0, 1), (-1, -1), 6),
('BOTTOMPADDING', (0, 1), (-1, -1), 6),
('BACKGROUND', (0, 1), (-1, -1), colors.white),
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 8),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#fadbd8')])
]))
server_elements.append(format_table)
server_elements.append(Spacer(1, 0.2*inch))
# Keep server results together when possible
elements.append(KeepTogether(server_elements))
elements.append(Spacer(1, 0.3*inch))
else:
no_issues = Paragraph(
"<i>No duplicate format keys found in any server.</i>",
normal_style
)
elements.append(no_issues)
# Build PDF
doc.build(elements)
print(f"✓ PDF report generated: {output_file}")
def main():
"""Main execution function."""
# Determine input and output files
input_file = "audit_duplicate_keys_results.json"
output_file = "audit_duplicate_keys_report.pdf"
if len(sys.argv) > 1:
input_file = sys.argv[1]
if len(sys.argv) > 2:
output_file = sys.argv[2]
else:
# Generate output filename from input filename
if input_file.endswith('.json'):
output_file = input_file[:-5] + '_report.pdf'
print(f"Reading results from: {input_file}")
results = load_results(input_file)
print(f"Generating PDF report...")
create_pdf_report(results, output_file)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment