Soumik Bose commited on
Commit ·
c2a315b
1
Parent(s): d54e4f5
go
Browse files- pdf_report_generation_service.py +148 -11
pdf_report_generation_service.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
-
Professional Q4 Report Generator -
|
| 3 |
-
Generates executive-quality PDF reports with proper
|
| 4 |
"""
|
| 5 |
import markdown
|
| 6 |
from weasyprint import HTML, CSS
|
|
@@ -22,7 +22,7 @@ logging.basicConfig(
|
|
| 22 |
logger = logging.getLogger(__name__)
|
| 23 |
|
| 24 |
# ---------------------------------------------------------
|
| 25 |
-
# EXECUTIVE Q4 REPORT THEME
|
| 26 |
# ---------------------------------------------------------
|
| 27 |
CSS_STYLE = """
|
| 28 |
@page {
|
|
@@ -253,20 +253,72 @@ blockquote {
|
|
| 253 |
page-break-after: always;
|
| 254 |
}
|
| 255 |
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
}
|
| 260 |
|
| 261 |
li {
|
| 262 |
-
margin:
|
| 263 |
line-height: 1.6;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
}
|
| 265 |
|
| 266 |
strong {
|
| 267 |
font-weight: 600;
|
| 268 |
color: #000;
|
| 269 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
"""
|
| 271 |
|
| 272 |
# ---------------------------------------------------------
|
|
@@ -371,16 +423,33 @@ class Q4ReportGenerator:
|
|
| 371 |
"""
|
| 372 |
|
| 373 |
def _process_section(self, section: Dict, output_dir: str) -> str:
|
|
|
|
| 374 |
html = ""
|
| 375 |
|
| 376 |
content = section.get("content", "")
|
| 377 |
if content:
|
|
|
|
| 378 |
html_content = markdown.markdown(
|
| 379 |
content,
|
| 380 |
-
extensions=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
)
|
| 382 |
html += f"<div class='section'>{html_content}</div>"
|
| 383 |
|
|
|
|
| 384 |
images = section.get("images", [])
|
| 385 |
for img_data in images:
|
| 386 |
if isinstance(img_data, dict):
|
|
@@ -401,6 +470,7 @@ class Q4ReportGenerator:
|
|
| 401 |
</div>
|
| 402 |
"""
|
| 403 |
|
|
|
|
| 404 |
if section.get("page_break", False):
|
| 405 |
html += '<div class="page-break"></div>'
|
| 406 |
|
|
@@ -411,20 +481,24 @@ class Q4ReportGenerator:
|
|
| 411 |
filename: str = "Q4_Report.pdf",
|
| 412 |
output_dir: str = "./output",
|
| 413 |
cover_page: bool = True) -> bool:
|
|
|
|
| 414 |
try:
|
| 415 |
os.makedirs(output_dir, exist_ok=True)
|
| 416 |
output_path = os.path.join(output_dir, filename)
|
| 417 |
|
| 418 |
logger.info(f"Starting Q4 report generation: {output_path}")
|
| 419 |
|
|
|
|
| 420 |
body_html = ""
|
| 421 |
if cover_page:
|
| 422 |
body_html += self._build_cover_page()
|
| 423 |
|
|
|
|
| 424 |
for idx, section in enumerate(sections):
|
| 425 |
logger.info(f"Processing section {idx + 1}/{len(sections)}")
|
| 426 |
body_html += self._process_section(section, output_dir)
|
| 427 |
|
|
|
|
| 428 |
full_html = f"""
|
| 429 |
<!DOCTYPE html>
|
| 430 |
<html>
|
|
@@ -440,15 +514,78 @@ class Q4ReportGenerator:
|
|
| 440 |
</html>
|
| 441 |
"""
|
| 442 |
|
|
|
|
| 443 |
logger.info("Rendering PDF with WeasyPrint...")
|
| 444 |
html_doc = HTML(string=full_html)
|
| 445 |
css_doc = CSS(string=CSS_STYLE)
|
| 446 |
|
| 447 |
html_doc.write_pdf(output_path, stylesheets=[css_doc])
|
| 448 |
|
| 449 |
-
logger.info(f"PDF Generated: {output_path}")
|
| 450 |
return True
|
| 451 |
|
| 452 |
except Exception as e:
|
| 453 |
-
logger.error(f"Error generating report: {e}", exc_info=True)
|
| 454 |
-
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
Professional Q4 Report Generator - Enhanced Markdown Parsing
|
| 3 |
+
Generates executive-quality PDF reports with proper list rendering
|
| 4 |
"""
|
| 5 |
import markdown
|
| 6 |
from weasyprint import HTML, CSS
|
|
|
|
| 22 |
logger = logging.getLogger(__name__)
|
| 23 |
|
| 24 |
# ---------------------------------------------------------
|
| 25 |
+
# EXECUTIVE Q4 REPORT THEME - ENHANCED LIST STYLING
|
| 26 |
# ---------------------------------------------------------
|
| 27 |
CSS_STYLE = """
|
| 28 |
@page {
|
|
|
|
| 253 |
page-break-after: always;
|
| 254 |
}
|
| 255 |
|
| 256 |
+
/* ENHANCED LIST STYLING - ENSURES PROPER BULLET RENDERING */
|
| 257 |
+
ul {
|
| 258 |
+
margin: 16px 0;
|
| 259 |
+
padding-left: 35px;
|
| 260 |
+
list-style-type: disc;
|
| 261 |
+
list-style-position: outside;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
ol {
|
| 265 |
+
margin: 16px 0;
|
| 266 |
+
padding-left: 35px;
|
| 267 |
+
list-style-type: decimal;
|
| 268 |
+
list-style-position: outside;
|
| 269 |
}
|
| 270 |
|
| 271 |
li {
|
| 272 |
+
margin: 6px 0;
|
| 273 |
line-height: 1.6;
|
| 274 |
+
padding-left: 5px;
|
| 275 |
+
display: list-item;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
ul ul {
|
| 279 |
+
margin: 8px 0;
|
| 280 |
+
padding-left: 25px;
|
| 281 |
+
list-style-type: circle;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
ul ul ul {
|
| 285 |
+
list-style-type: square;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
ol ol {
|
| 289 |
+
margin: 8px 0;
|
| 290 |
+
padding-left: 25px;
|
| 291 |
+
list-style-type: lower-alpha;
|
| 292 |
}
|
| 293 |
|
| 294 |
strong {
|
| 295 |
font-weight: 600;
|
| 296 |
color: #000;
|
| 297 |
}
|
| 298 |
+
|
| 299 |
+
em {
|
| 300 |
+
font-style: italic;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
code {
|
| 304 |
+
font-family: 'Courier New', monospace;
|
| 305 |
+
background-color: #f4f4f4;
|
| 306 |
+
padding: 2px 6px;
|
| 307 |
+
border-radius: 3px;
|
| 308 |
+
font-size: 10pt;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
pre {
|
| 312 |
+
background-color: #f4f4f4;
|
| 313 |
+
border: 1px solid #ddd;
|
| 314 |
+
border-radius: 4px;
|
| 315 |
+
padding: 12px;
|
| 316 |
+
overflow-x: auto;
|
| 317 |
+
font-family: 'Courier New', monospace;
|
| 318 |
+
font-size: 9.5pt;
|
| 319 |
+
line-height: 1.4;
|
| 320 |
+
margin: 16px 0;
|
| 321 |
+
}
|
| 322 |
"""
|
| 323 |
|
| 324 |
# ---------------------------------------------------------
|
|
|
|
| 423 |
"""
|
| 424 |
|
| 425 |
def _process_section(self, section: Dict, output_dir: str) -> str:
|
| 426 |
+
"""Process section with enhanced markdown parsing for proper list rendering."""
|
| 427 |
html = ""
|
| 428 |
|
| 429 |
content = section.get("content", "")
|
| 430 |
if content:
|
| 431 |
+
# Use comprehensive markdown extensions for full parsing
|
| 432 |
html_content = markdown.markdown(
|
| 433 |
content,
|
| 434 |
+
extensions=[
|
| 435 |
+
'extra', # Tables, footnotes, attributes
|
| 436 |
+
'sane_lists', # Better list handling
|
| 437 |
+
'tables', # Table support
|
| 438 |
+
'fenced_code', # Code blocks
|
| 439 |
+
'nl2br', # Newline to <br>
|
| 440 |
+
'codehilite', # Syntax highlighting
|
| 441 |
+
'toc' # Table of contents
|
| 442 |
+
],
|
| 443 |
+
extension_configs={
|
| 444 |
+
'codehilite': {
|
| 445 |
+
'guess_lang': False,
|
| 446 |
+
'noclasses': True
|
| 447 |
+
}
|
| 448 |
+
}
|
| 449 |
)
|
| 450 |
html += f"<div class='section'>{html_content}</div>"
|
| 451 |
|
| 452 |
+
# Handle images
|
| 453 |
images = section.get("images", [])
|
| 454 |
for img_data in images:
|
| 455 |
if isinstance(img_data, dict):
|
|
|
|
| 470 |
</div>
|
| 471 |
"""
|
| 472 |
|
| 473 |
+
# Page break if requested
|
| 474 |
if section.get("page_break", False):
|
| 475 |
html += '<div class="page-break"></div>'
|
| 476 |
|
|
|
|
| 481 |
filename: str = "Q4_Report.pdf",
|
| 482 |
output_dir: str = "./output",
|
| 483 |
cover_page: bool = True) -> bool:
|
| 484 |
+
"""Generate PDF report with proper markdown parsing."""
|
| 485 |
try:
|
| 486 |
os.makedirs(output_dir, exist_ok=True)
|
| 487 |
output_path = os.path.join(output_dir, filename)
|
| 488 |
|
| 489 |
logger.info(f"Starting Q4 report generation: {output_path}")
|
| 490 |
|
| 491 |
+
# Build HTML body
|
| 492 |
body_html = ""
|
| 493 |
if cover_page:
|
| 494 |
body_html += self._build_cover_page()
|
| 495 |
|
| 496 |
+
# Process all sections
|
| 497 |
for idx, section in enumerate(sections):
|
| 498 |
logger.info(f"Processing section {idx + 1}/{len(sections)}")
|
| 499 |
body_html += self._process_section(section, output_dir)
|
| 500 |
|
| 501 |
+
# Construct full HTML document
|
| 502 |
full_html = f"""
|
| 503 |
<!DOCTYPE html>
|
| 504 |
<html>
|
|
|
|
| 514 |
</html>
|
| 515 |
"""
|
| 516 |
|
| 517 |
+
# Generate PDF
|
| 518 |
logger.info("Rendering PDF with WeasyPrint...")
|
| 519 |
html_doc = HTML(string=full_html)
|
| 520 |
css_doc = CSS(string=CSS_STYLE)
|
| 521 |
|
| 522 |
html_doc.write_pdf(output_path, stylesheets=[css_doc])
|
| 523 |
|
| 524 |
+
logger.info(f"✓ PDF Generated Successfully: {output_path}")
|
| 525 |
return True
|
| 526 |
|
| 527 |
except Exception as e:
|
| 528 |
+
logger.error(f"✗ Error generating report: {e}", exc_info=True)
|
| 529 |
+
return False
|
| 530 |
+
|
| 531 |
+
def cleanup_images(self, output_dir: str):
|
| 532 |
+
"""Optional: Clean up downloaded images after PDF generation."""
|
| 533 |
+
try:
|
| 534 |
+
img_dir = os.path.join(output_dir, 'images')
|
| 535 |
+
if os.path.exists(img_dir):
|
| 536 |
+
import shutil
|
| 537 |
+
shutil.rmtree(img_dir)
|
| 538 |
+
logger.info("Cleaned up temporary images")
|
| 539 |
+
except Exception as e:
|
| 540 |
+
logger.warning(f"Failed to cleanup images: {e}")
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
# ---------------------------------------------------------
|
| 544 |
+
# USAGE EXAMPLE
|
| 545 |
+
# ---------------------------------------------------------
|
| 546 |
+
# if __name__ == "__main__":
|
| 547 |
+
# # Example usage
|
| 548 |
+
# generator = Q4ReportGenerator(
|
| 549 |
+
# title="Q4 2024 Performance Report",
|
| 550 |
+
# subtitle="Product Analytics Summary",
|
| 551 |
+
# author="Analytics Team",
|
| 552 |
+
# department="Product Department"
|
| 553 |
+
# )
|
| 554 |
+
|
| 555 |
+
# sections = [
|
| 556 |
+
# {
|
| 557 |
+
# "content": """
|
| 558 |
+
# # Executive Summary
|
| 559 |
+
|
| 560 |
+
# This report provides a comprehensive analysis of Q4 2024 product performance.
|
| 561 |
+
|
| 562 |
+
# ## Key Highlights
|
| 563 |
+
|
| 564 |
+
# * Revenue increased by 23% compared to Q3
|
| 565 |
+
# * Customer satisfaction improved to 4.7/5.0
|
| 566 |
+
# * Successfully launched 3 new product features
|
| 567 |
+
|
| 568 |
+
# ## Daily Product Additions
|
| 569 |
+
|
| 570 |
+
# Example Daily Additions:
|
| 571 |
+
|
| 572 |
+
# * 2023-09-30: 3 products
|
| 573 |
+
# * 2023-10-01: 1 product
|
| 574 |
+
# * 2023-10-02: 4 products
|
| 575 |
+
# * 2024-09-27: 3 products
|
| 576 |
+
# * 2024-09-28: 3 products
|
| 577 |
+
# * 2024-09-29: 2 products
|
| 578 |
+
|
| 579 |
+
# ## Growth Metrics
|
| 580 |
+
|
| 581 |
+
# 1. User acquisition up 45%
|
| 582 |
+
# 2. Retention rate improved to 87%
|
| 583 |
+
# 3. Average session time increased by 12 minutes
|
| 584 |
+
# """,
|
| 585 |
+
# "page_break": False
|
| 586 |
+
# }
|
| 587 |
+
# ]
|
| 588 |
+
|
| 589 |
+
# success = generator.generate(sections, filename="Q4_Report.pdf")
|
| 590 |
+
# if success:
|
| 591 |
+
# print("Report generated successfully!")
|