import io
import zipfile
from datetime import datetime
import base64
from pathlib import Path
import numpy as np
[docs]
class HTMLReportGenerator:
[docs]
def __init__(self, session_data):
"""Initialize with session data"""
self.session_data = session_data
def _create_html(self) -> str:
"""Create HTML report"""
# Get timestamp
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Start HTML content
html_content = f'''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Malva Search Report - {timestamp}</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
color: #333;
}}
h1, h2, h3 {{
color: #2c3e50;
margin-top: 1.5em;
}}
.header {{
border-bottom: 2px solid #eee;
padding-bottom: 10px;
margin-bottom: 30px;
}}
.timestamp {{
color: #666;
font-size: 0.9em;
}}
.info-table {{
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}}
.info-table th, .info-table td {{
padding: 12px;
border: 1px solid #ddd;
text-align: left;
}}
.info-table th {{
background-color: #f5f6fa;
}}
.visualization {{
margin: 30px 0;
text-align: center;
}}
.sequence-info {{
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
overflow-x: auto;
}}
.sequence {{
font-family: monospace;
white-space: pre-wrap;
word-wrap: break-word;
}}
.note {{
background-color: #fff3cd;
border: 1px solid #ffeeba;
color: #856404;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
}}
.footer {{
margin-top: 50px;
padding-top: 20px;
border-top: 2px solid #eee;
font-size: 0.9em;
color: #666;
}}
</style>
</head>
<body>
<div class="header">
<h1>Malva Search Report</h1>
<div class="timestamp">Generated on: {timestamp}</div>
</div>
<h2>Query Information</h2>
<table class="info-table">
<tr>
<th>Query Term</th>
<td>{self.session_data.get("query_term", "N/A")}</td>
</tr>
<tr>
<th>Window Size</th>
<td>{self.session_data.get("sliding_size", "N/A")}</td>
</tr>
<tr>
<th>Analysis Parameters</th>
<td>
Low Complexity Filter: {self.session_data.get("low_complexity_filter", "N/A")}<br>
K-mer Presence Threshold: {self.session_data.get("pct_threshold", "N/A")}
</td>
</tr>
</table>
<h2>Sequences</h2>
<div class="sequence-info">
<pre class="sequence">{self._format_sequences()}</pre>
</div>
'''
# Add visualization section if available
if "where_abundant" in self.session_data:
html_content += self._create_visualization_section()
# Add coverage plot placeholder
html_content += '''
<h2>Sequence Coverage</h2>
<div class="note">
Coverage plot showing query sequence matches will be available in future updates.
</div>
'''
# Add file information
html_content += '''
<h2>Additional Files</h2>
<table class="info-table">
<tr>
<th>File</th>
<th>Description</th>
</tr>
<tr>
<td>queries.fa</td>
<td>Contains all query sequences in FASTA format</td>
</tr>
<tr>
<td>malva.log</td>
<td>Contains detailed logging information from the analysis</td>
</tr>
<tr>
<td>summary.txt</td>
<td>Contains additional summary statistics and analysis details</td>
</tr>
</table>
<div class="footer">
<p>Generated by Malva Search Tool</p>
<p>For more information, visit the documentation.</p>
</div>
</body>
</html>
'''
return html_content
def _format_sequences(self) -> str:
"""Format sequences for display"""
sequences = self._get_sequences()
formatted = []
for idx, seq in enumerate(sequences, 1):
if isinstance(seq, dict) and 'header' in seq:
header = seq['header']
sequence = seq['sequence']
else:
if self.session_data.get("query_term", "").startswith(("gene:", "ensembl:")):
header = f">query_{self.session_data['query_term']}"
else:
header = f">query_sequence_{idx}"
sequence = seq
# Format sequence with line breaks every 60 characters
formatted_seq = '\n'.join(sequence[i:i+60] for i in range(0, len(sequence), 60))
formatted.append(f"{header}\n{formatted_seq}")
return '\n\n'.join(formatted)
def _create_visualization_section(self) -> str:
"""Create visualization section of the report"""
# Add placeholder for static visualization image
return '''
<h2>Search Results Visualization</h2>
<div class="visualization">
<p>The interactive visualization from the web interface is available in the results view.</p>
<!-- Static visualization image would be added here -->
</div>
'''
def _get_sequences(self) -> list:
"""Extract sequences from session data"""
sequences = []
query_seq = self.session_data.get("query_seq")
if isinstance(query_seq, list):
sequences.extend(query_seq)
else:
sequences.append(query_seq)
return sequences
def _create_fasta(self) -> str:
"""Create FASTA format file"""
sequences = self._get_sequences()
fasta_content = []
for idx, seq in enumerate(sequences, 1):
if isinstance(seq, dict) and 'header' in seq:
header = seq['header']
sequence = seq['sequence']
else:
if self.session_data.get("query_term", "").startswith(("gene:", "ensembl:")):
header = f">query_{self.session_data['query_term']}"
else:
header = f">query_sequence_{idx}"
sequence = seq
fasta_content.append(f"{header}\n{sequence}")
return "\n".join(fasta_content)
def _create_summary(self) -> str:
"""Create summary text file"""
summary_lines = [
"MALVA SEARCH SUMMARY",
"=" * 50,
f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"Query Term: {self.session_data.get('query_term', 'N/A')}",
f"Number of Sequences: {len(self._get_sequences())}",
f"Window Size: {self.session_data.get('sliding_size', 'N/A')}",
"\nSearch Parameters:",
"-" * 20,
f"Low Complexity Filter: {self.session_data.get('low_complexity_filter', 'N/A')}",
f"K-mer Presence Threshold: {self.session_data.get('pct_threshold', 'N/A')}",
]
return "\n".join(summary_lines)
[docs]
def create_report_zip(self) -> bytes:
"""Create ZIP file containing all report components"""
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
# Add HTML report
zip_file.writestr('report.html', self._create_html())
# Add FASTA file
zip_file.writestr('queries.fa', self._create_fasta())
# Add log file
zip_file.writestr('malva.log', "Malva log content will be added here\n")
# Add summary file
zip_file.writestr('summary.txt', self._create_summary())
zip_buffer.seek(0)
return zip_buffer.getvalue()