Soumik Bose commited on
Commit
58b47de
·
1 Parent(s): c2a315b
Files changed (1) hide show
  1. pdf_report_generation_service.py +188 -69
pdf_report_generation_service.py CHANGED
@@ -1,11 +1,12 @@
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
7
  import requests
8
  import os
 
9
  from datetime import datetime
10
  from typing import List, Dict, Optional
11
  import logging
@@ -22,7 +23,7 @@ logging.basicConfig(
22
  logger = logging.getLogger(__name__)
23
 
24
  # ---------------------------------------------------------
25
- # EXECUTIVE Q4 REPORT THEME - ENHANCED LIST STYLING
26
  # ---------------------------------------------------------
27
  CSS_STYLE = """
28
  @page {
@@ -144,6 +145,38 @@ h3 {
144
  font-weight: 600;
145
  }
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  table {
148
  width: 100%;
149
  border-collapse: collapse;
@@ -190,6 +223,14 @@ tr:nth-child(odd) {
190
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
191
  }
192
 
 
 
 
 
 
 
 
 
193
  .key-metric-box {
194
  background: linear-gradient(135deg, #003366 0%, #0055a5 100%);
195
  color: white;
@@ -200,11 +241,25 @@ tr:nth-child(odd) {
200
  box-shadow: 0 3px 6px rgba(0,0,0,0.15);
201
  }
202
 
203
- .key-metric-box h3 {
 
 
204
  color: white;
205
  margin-top: 0;
206
  }
207
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  blockquote {
209
  border-left: 5px solid #003366;
210
  margin-left: 0;
@@ -217,6 +272,19 @@ blockquote {
217
  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
218
  }
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  .figure-container {
221
  text-align: center;
222
  margin: 24px 0;
@@ -249,11 +317,19 @@ blockquote {
249
  border-radius: 5px;
250
  }
251
 
 
 
 
 
 
 
 
 
252
  .page-break {
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;
@@ -275,6 +351,10 @@ li {
275
  display: list-item;
276
  }
277
 
 
 
 
 
278
  ul ul {
279
  margin: 8px 0;
280
  padding-left: 25px;
@@ -291,12 +371,33 @@ ol ol {
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
 
@@ -306,6 +407,7 @@ code {
306
  padding: 2px 6px;
307
  border-radius: 3px;
308
  font-size: 10pt;
 
309
  }
310
 
311
  pre {
@@ -319,6 +421,27 @@ pre {
319
  line-height: 1.4;
320
  margin: 16px 0;
321
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  """
323
 
324
  # ---------------------------------------------------------
@@ -372,11 +495,39 @@ def fetch_image(url: str, output_dir: str) -> Optional[str]:
372
  logger.warning(f"Failed to download image: {url[:50]}... - {e}")
373
  return None
374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  # ---------------------------------------------------------
376
  # Q4 REPORT GENERATOR CLASS
377
  # ---------------------------------------------------------
378
  class Q4ReportGenerator:
379
- """Generates executive-quality Q4 reports using WeasyPrint."""
380
 
381
  def __init__(self,
382
  title: str = "Q4 Performance Report",
@@ -423,30 +574,47 @@ class Q4ReportGenerator:
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
@@ -481,12 +649,13 @@ class Q4ReportGenerator:
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 = ""
@@ -522,6 +691,7 @@ class Q4ReportGenerator:
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:
@@ -538,54 +708,3 @@ class Q4ReportGenerator:
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!")
 
1
  """
2
+ Professional Q4 Report Generator - Complete Markdown Parsing
3
+ Ensures ALL markdown syntax is properly rendered in PDF reports
4
  """
5
  import markdown
6
  from weasyprint import HTML, CSS
7
  import requests
8
  import os
9
+ import re
10
  from datetime import datetime
11
  from typing import List, Dict, Optional
12
  import logging
 
23
  logger = logging.getLogger(__name__)
24
 
25
  # ---------------------------------------------------------
26
+ # EXECUTIVE Q4 REPORT THEME - COMPLETE STYLING
27
  # ---------------------------------------------------------
28
  CSS_STYLE = """
29
  @page {
 
145
  font-weight: 600;
146
  }
147
 
148
+ h4 {
149
+ color: #555;
150
+ font-size: 12pt;
151
+ margin-top: 16px;
152
+ margin-bottom: 10px;
153
+ page-break-after: avoid;
154
+ font-weight: 600;
155
+ }
156
+
157
+ h5 {
158
+ color: #666;
159
+ font-size: 11.5pt;
160
+ margin-top: 14px;
161
+ margin-bottom: 8px;
162
+ page-break-after: avoid;
163
+ font-weight: 600;
164
+ }
165
+
166
+ h6 {
167
+ color: #777;
168
+ font-size: 11pt;
169
+ margin-top: 12px;
170
+ margin-bottom: 8px;
171
+ page-break-after: avoid;
172
+ font-weight: 600;
173
+ }
174
+
175
+ p {
176
+ margin: 10px 0;
177
+ line-height: 1.65;
178
+ }
179
+
180
  table {
181
  width: 100%;
182
  border-collapse: collapse;
 
223
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
224
  }
225
 
226
+ .highlight-box p:first-child {
227
+ margin-top: 0;
228
+ }
229
+
230
+ .highlight-box p:last-child {
231
+ margin-bottom: 0;
232
+ }
233
+
234
  .key-metric-box {
235
  background: linear-gradient(135deg, #003366 0%, #0055a5 100%);
236
  color: white;
 
241
  box-shadow: 0 3px 6px rgba(0,0,0,0.15);
242
  }
243
 
244
+ .key-metric-box h3,
245
+ .key-metric-box h4,
246
+ .key-metric-box h5 {
247
  color: white;
248
  margin-top: 0;
249
  }
250
 
251
+ .key-metric-box p:first-child {
252
+ margin-top: 0;
253
+ }
254
+
255
+ .key-metric-box p:last-child {
256
+ margin-bottom: 0;
257
+ }
258
+
259
+ .key-metric-box strong {
260
+ color: #ffffff;
261
+ }
262
+
263
  blockquote {
264
  border-left: 5px solid #003366;
265
  margin-left: 0;
 
272
  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
273
  }
274
 
275
+ blockquote p:first-child {
276
+ margin-top: 0;
277
+ }
278
+
279
+ blockquote p:last-child {
280
+ margin-bottom: 0;
281
+ }
282
+
283
+ blockquote strong {
284
+ font-weight: 700;
285
+ color: #000;
286
+ }
287
+
288
  .figure-container {
289
  text-align: center;
290
  margin: 24px 0;
 
317
  border-radius: 5px;
318
  }
319
 
320
+ .executive-summary p:first-child {
321
+ margin-top: 0;
322
+ }
323
+
324
+ .executive-summary p:last-child {
325
+ margin-bottom: 0;
326
+ }
327
+
328
  .page-break {
329
  page-break-after: always;
330
  }
331
 
332
+ /* COMPREHENSIVE LIST STYLING */
333
  ul {
334
  margin: 16px 0;
335
  padding-left: 35px;
 
351
  display: list-item;
352
  }
353
 
354
+ li p {
355
+ margin: 4px 0;
356
+ }
357
+
358
  ul ul {
359
  margin: 8px 0;
360
  padding-left: 25px;
 
371
  list-style-type: lower-alpha;
372
  }
373
 
374
+ ol ol ol {
375
+ list-style-type: lower-roman;
376
+ }
377
+
378
+ /* Lists inside special boxes */
379
+ .highlight-box ul,
380
+ .highlight-box ol,
381
+ .key-metric-box ul,
382
+ .key-metric-box ol,
383
+ .executive-summary ul,
384
+ .executive-summary ol,
385
+ blockquote ul,
386
+ blockquote ol {
387
+ margin: 12px 0;
388
+ }
389
+
390
+ .key-metric-box li {
391
+ color: white;
392
+ }
393
+
394
+ /* TEXT FORMATTING */
395
+ strong, b {
396
+ font-weight: 700;
397
  color: #000;
398
  }
399
 
400
+ em, i {
401
  font-style: italic;
402
  }
403
 
 
407
  padding: 2px 6px;
408
  border-radius: 3px;
409
  font-size: 10pt;
410
+ color: #c7254e;
411
  }
412
 
413
  pre {
 
421
  line-height: 1.4;
422
  margin: 16px 0;
423
  }
424
+
425
+ pre code {
426
+ background-color: transparent;
427
+ padding: 0;
428
+ color: #333;
429
+ }
430
+
431
+ a {
432
+ color: #0055a5;
433
+ text-decoration: none;
434
+ }
435
+
436
+ a:hover {
437
+ text-decoration: underline;
438
+ }
439
+
440
+ hr {
441
+ border: none;
442
+ border-top: 2px solid #ddd;
443
+ margin: 24px 0;
444
+ }
445
  """
446
 
447
  # ---------------------------------------------------------
 
495
  logger.warning(f"Failed to download image: {url[:50]}... - {e}")
496
  return None
497
 
498
+ # ---------------------------------------------------------
499
+ # MARKDOWN PREPROCESSING
500
+ # ---------------------------------------------------------
501
+ def preprocess_markdown(content: str) -> str:
502
+ """
503
+ Preprocess markdown content to ensure proper parsing inside HTML blocks.
504
+ Adds markdown="1" attribute to known HTML container tags.
505
+ """
506
+ # List of container tags that might contain markdown
507
+ container_tags = ['div', 'blockquote', 'section', 'article', 'aside']
508
+
509
+ for tag in container_tags:
510
+ # Pattern to find opening tags without markdown attribute
511
+ # Matches: <div class="something"> or <div> but not <div markdown="1">
512
+ pattern = rf'<{tag}(\s+[^>]*?)?(?!\s+markdown\s*=)>'
513
+
514
+ def add_markdown_attr(match):
515
+ attrs = match.group(1) or ''
516
+ # If there are existing attributes, add markdown after them
517
+ if attrs:
518
+ return f'<{tag}{attrs} markdown="1">'
519
+ else:
520
+ return f'<{tag} markdown="1">'
521
+
522
+ content = re.sub(pattern, add_markdown_attr, content, flags=re.IGNORECASE)
523
+
524
+ return content
525
+
526
  # ---------------------------------------------------------
527
  # Q4 REPORT GENERATOR CLASS
528
  # ---------------------------------------------------------
529
  class Q4ReportGenerator:
530
+ """Generates executive-quality Q4 reports using WeasyPrint with complete markdown support."""
531
 
532
  def __init__(self,
533
  title: str = "Q4 Performance Report",
 
574
  """
575
 
576
  def _process_section(self, section: Dict, output_dir: str) -> str:
577
+ """
578
+ Process section with COMPLETE markdown parsing support.
579
+ Handles markdown both outside and inside HTML tags.
580
+ """
581
  html = ""
582
 
583
  content = section.get("content", "")
584
  if content:
585
+ # Step 1: Preprocess to add markdown="1" to HTML containers
586
+ preprocessed_content = preprocess_markdown(content)
587
+
588
+ # Step 2: Apply comprehensive markdown parsing
589
  html_content = markdown.markdown(
590
+ preprocessed_content,
591
  extensions=[
592
+ 'extra', # Tables, footnotes, attributes, etc.
593
  'sane_lists', # Better list handling
594
  'tables', # Table support
595
+ 'fenced_code', # Code blocks with ```
596
+ 'nl2br', # Convert newlines to <br>
597
  'codehilite', # Syntax highlighting
598
+ 'toc', # Table of contents
599
+ 'md_in_html', # CRITICAL: Parse markdown inside HTML tags
600
+ 'attr_list', # Add attributes to elements
601
+ 'def_list', # Definition lists
602
+ 'abbr', # Abbreviations
603
+ 'footnotes', # Footnotes
604
+ 'smarty' # Smart quotes and dashes
605
  ],
606
  extension_configs={
607
  'codehilite': {
608
  'guess_lang': False,
609
+ 'noclasses': True,
610
+ 'css_class': 'highlight'
611
+ },
612
+ 'md_in_html': {
613
+ 'allowed_elements': ['div', 'blockquote', 'section', 'article', 'aside', 'span']
614
  }
615
  }
616
  )
617
+
618
  html += f"<div class='section'>{html_content}</div>"
619
 
620
  # Handle images
 
649
  filename: str = "Q4_Report.pdf",
650
  output_dir: str = "./output",
651
  cover_page: bool = True) -> bool:
652
+ """Generate PDF report with complete markdown parsing everywhere."""
653
  try:
654
  os.makedirs(output_dir, exist_ok=True)
655
  output_path = os.path.join(output_dir, filename)
656
 
657
  logger.info(f"Starting Q4 report generation: {output_path}")
658
+ logger.info("Markdown parsing: FULL (inside and outside HTML tags)")
659
 
660
  # Build HTML body
661
  body_html = ""
 
691
  html_doc.write_pdf(output_path, stylesheets=[css_doc])
692
 
693
  logger.info(f"✓ PDF Generated Successfully: {output_path}")
694
+ logger.info(f"✓ All markdown syntax properly parsed")
695
  return True
696
 
697
  except Exception as e:
 
708
  logger.info("Cleaned up temporary images")
709
  except Exception as e:
710
  logger.warning(f"Failed to cleanup images: {e}")