Soumik Bose commited on
Commit
fef3466
·
1 Parent(s): 63d66b6
Files changed (1) hide show
  1. pdf_report_generation_service.py +90 -108
pdf_report_generation_service.py CHANGED
@@ -1,12 +1,13 @@
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,7 +24,7 @@ logging.basicConfig(
23
  logger = logging.getLogger(__name__)
24
 
25
  # ---------------------------------------------------------
26
- # EXECUTIVE Q4 REPORT THEME - COMPLETE STYLING
27
  # ---------------------------------------------------------
28
  CSS_STYLE = """
29
  @page {
@@ -41,7 +42,7 @@ CSS_STYLE = """
41
 
42
  body {
43
  font-family: 'Helvetica', Arial, sans-serif;
44
- color: #1a1a1a;
45
  line-height: 1.65;
46
  font-size: 11pt;
47
  }
@@ -49,8 +50,8 @@ body {
49
  .header {
50
  position: running(header);
51
  font-size: 9pt;
52
- color: #666;
53
- border-bottom: 1px solid #ddd;
54
  padding-bottom: 6px;
55
  margin-bottom: 20px;
56
  }
@@ -58,8 +59,8 @@ body {
58
  .footer {
59
  position: running(footer);
60
  font-size: 9pt;
61
- color: #666;
62
- border-top: 1px solid #ddd;
63
  padding-top: 6px;
64
  text-align: center;
65
  }
@@ -69,7 +70,7 @@ body {
69
  }
70
 
71
  .confidential-mark {
72
- color: #d32f2f;
73
  font-weight: bold;
74
  float: left;
75
  }
@@ -83,7 +84,7 @@ body {
83
  .cover-title {
84
  font-size: 36pt;
85
  font-weight: bold;
86
- color: #003366;
87
  margin-bottom: 0.5in;
88
  letter-spacing: 1px;
89
  line-height: 1.2;
@@ -91,35 +92,35 @@ body {
91
 
92
  .cover-subtitle {
93
  font-size: 20pt;
94
- color: #0055a5;
95
  margin-bottom: 1.2in;
96
  font-weight: 500;
97
  }
98
 
99
  .cover-meta {
100
  font-size: 15pt;
101
- color: #555;
102
  margin-bottom: 0.25in;
103
  line-height: 1.5;
104
  }
105
 
106
  .cover-date {
107
  font-size: 12pt;
108
- color: #888;
109
  margin-top: 0.5in;
110
  }
111
 
112
  .cover-divider {
113
  width: 3in;
114
  height: 3px;
115
- background: linear-gradient(to right, #003366, #0055a5);
116
  margin: 0.5in auto;
117
  }
118
 
119
  h1 {
120
- color: #003366;
121
  font-size: 20pt;
122
- border-bottom: 3px solid #0055a5;
123
  padding-bottom: 10px;
124
  margin-top: 28px;
125
  margin-bottom: 18px;
@@ -128,7 +129,7 @@ h1 {
128
  }
129
 
130
  h2 {
131
- color: #0055a5;
132
  font-size: 15pt;
133
  margin-top: 22px;
134
  margin-bottom: 14px;
@@ -137,7 +138,7 @@ h2 {
137
  }
138
 
139
  h3 {
140
- color: #333;
141
  font-size: 13pt;
142
  margin-top: 18px;
143
  margin-bottom: 12px;
@@ -146,7 +147,7 @@ h3 {
146
  }
147
 
148
  h4 {
149
- color: #555;
150
  font-size: 12pt;
151
  margin-top: 16px;
152
  margin-bottom: 10px;
@@ -155,7 +156,7 @@ h4 {
155
  }
156
 
157
  h5 {
158
- color: #666;
159
  font-size: 11.5pt;
160
  margin-top: 14px;
161
  margin-bottom: 8px;
@@ -164,7 +165,7 @@ h5 {
164
  }
165
 
166
  h6 {
167
- color: #777;
168
  font-size: 11pt;
169
  margin-top: 12px;
170
  margin-bottom: 8px;
@@ -183,31 +184,31 @@ table {
183
  margin: 18px 0;
184
  font-size: 10pt;
185
  page-break-inside: avoid;
186
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
187
  }
188
 
189
  thead {
190
- background: linear-gradient(to bottom, #003366, #00264d);
191
  }
192
 
193
  th {
194
- background-color: #003366;
195
  color: white;
196
  padding: 12px;
197
  text-align: left;
198
  font-weight: bold;
199
- border: 1px solid #002244;
200
  font-size: 10.5pt;
201
  }
202
 
203
  td {
204
  padding: 10px 12px;
205
- border: 1px solid #ddd;
206
  line-height: 1.5;
207
  }
208
 
209
  tr:nth-child(even) {
210
- background-color: #f8f9fa;
211
  }
212
 
213
  tr:nth-child(odd) {
@@ -215,12 +216,12 @@ tr:nth-child(odd) {
215
  }
216
 
217
  .highlight-box {
218
- background-color: #e8f4f8;
219
- border-left: 5px solid #0055a5;
220
  padding: 16px 20px;
221
  margin: 20px 0;
222
  page-break-inside: avoid;
223
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
224
  }
225
 
226
  .highlight-box p:first-child {
@@ -232,13 +233,13 @@ tr:nth-child(odd) {
232
  }
233
 
234
  .key-metric-box {
235
- background: linear-gradient(135deg, #003366 0%, #0055a5 100%);
236
  color: white;
237
  border-radius: 8px;
238
  padding: 18px 22px;
239
  margin: 20px 0;
240
  page-break-inside: avoid;
241
- box-shadow: 0 3px 6px rgba(0,0,0,0.15);
242
  }
243
 
244
  .key-metric-box h3,
@@ -261,15 +262,15 @@ tr:nth-child(odd) {
261
  }
262
 
263
  blockquote {
264
- border-left: 5px solid #003366;
265
  margin-left: 0;
266
  padding-left: 20px;
267
- color: #333;
268
  font-style: italic;
269
- background-color: #f8f9fa;
270
  padding: 14px 14px 14px 20px;
271
  margin: 18px 0;
272
- box-shadow: 0 1px 2px rgba(0,0,0,0.05);
273
  }
274
 
275
  blockquote p:first-child {
@@ -282,7 +283,7 @@ blockquote p:last-child {
282
 
283
  blockquote strong {
284
  font-weight: 700;
285
- color: #000;
286
  }
287
 
288
  .figure-container {
@@ -294,23 +295,23 @@ blockquote strong {
294
  .figure-container img {
295
  max-width: 100%;
296
  height: auto;
297
- border: 1px solid #ddd;
298
  padding: 8px;
299
  background: white;
300
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
301
  }
302
 
303
  .figure-caption {
304
  font-size: 9.5pt;
305
- color: #666;
306
  margin-top: 10px;
307
  font-style: italic;
308
  font-weight: 500;
309
  }
310
 
311
  .executive-summary {
312
- background-color: #f0f7ff;
313
- border: 2px solid #0055a5;
314
  padding: 20px;
315
  margin: 24px 0;
316
  page-break-inside: avoid;
@@ -394,7 +395,7 @@ blockquote ol {
394
  /* TEXT FORMATTING */
395
  strong, b {
396
  font-weight: 700;
397
- color: #000;
398
  }
399
 
400
  em, i {
@@ -403,16 +404,16 @@ em, i {
403
 
404
  code {
405
  font-family: 'Courier New', monospace;
406
- background-color: #f4f4f4;
407
  padding: 2px 6px;
408
  border-radius: 3px;
409
  font-size: 10pt;
410
- color: #c7254e;
411
  }
412
 
413
  pre {
414
- background-color: #f4f4f4;
415
- border: 1px solid #ddd;
416
  border-radius: 4px;
417
  padding: 12px;
418
  overflow-x: auto;
@@ -425,11 +426,11 @@ pre {
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
 
@@ -439,18 +440,25 @@ a:hover {
439
 
440
  hr {
441
  border: none;
442
- border-top: 2px solid #ddd;
443
  margin: 24px 0;
444
  }
445
  """
446
 
 
 
 
 
 
 
 
 
 
447
  # ---------------------------------------------------------
448
  # IMAGE HANDLING
449
  # ---------------------------------------------------------
450
  def fetch_image(url: str, output_dir: str) -> Optional[str]:
451
- """
452
- Download image from URL to persistent file for PDF rendering.
453
- """
454
  if not url.startswith(('http://', 'https://')):
455
  if os.path.exists(url):
456
  abs_path = os.path.abspath(url)
@@ -499,21 +507,14 @@ def fetch_image(url: str, output_dir: str) -> Optional[str]:
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:
@@ -527,7 +528,7 @@ def preprocess_markdown(content: str) -> str:
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,34 +575,19 @@ class Q4ReportGenerator:
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': {
@@ -635,7 +621,6 @@ class Q4ReportGenerator:
635
  </div>
636
  """
637
 
638
- # Page break if requested
639
  if section.get("page_break", False):
640
  html += '<div class="page-break"></div>'
641
 
@@ -643,23 +628,36 @@ class Q4ReportGenerator:
643
 
644
  def generate(self,
645
  sections: List[Dict],
646
- filename: str = "Q4_Report.pdf",
647
  output_dir: str = "./output",
648
- cover_page: bool = True) -> bool:
649
- """Generate PDF report with complete markdown parsing everywhere."""
 
 
 
650
  try:
651
  os.makedirs(output_dir, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
652
  output_path = os.path.join(output_dir, filename)
653
 
654
  logger.info(f"Starting Q4 report generation: {output_path}")
655
- logger.info("Markdown parsing: FULL (inside and outside HTML tags)")
656
 
657
  # Build HTML body
658
  body_html = ""
659
  if cover_page:
660
  body_html += self._build_cover_page()
661
 
662
- # Process all sections
663
  for idx, section in enumerate(sections):
664
  logger.info(f"Processing section {idx + 1}/{len(sections)}")
665
  body_html += self._process_section(section, output_dir)
@@ -688,12 +686,11 @@ class Q4ReportGenerator:
688
  html_doc.write_pdf(output_path, stylesheets=[css_doc])
689
 
690
  logger.info(f"✓ PDF Generated Successfully: {output_path}")
691
- logger.info(f"✓ All markdown syntax properly parsed")
692
- return True
693
 
694
  except Exception as e:
695
  logger.error(f"✗ Error generating report: {e}", exc_info=True)
696
- return False
697
 
698
  def cleanup_images(self, output_dir: str):
699
  """Optional: Clean up downloaded images after PDF generation."""
@@ -711,7 +708,6 @@ class Q4ReportGenerator:
711
  # EXAMPLE USAGE
712
  # ---------------------------------------------------------
713
  if __name__ == "__main__":
714
- # Example of how to use the generator
715
  generator = Q4ReportGenerator(
716
  title="Q4 2024 Performance Report",
717
  subtitle="Executive Summary & Analysis",
@@ -748,30 +744,16 @@ This report presents our **Q4 2024** performance metrics.
748
  </div>
749
  """,
750
  "page_break": False
751
- },
752
- {
753
- "content": """
754
- # Strategic Recommendations
755
-
756
- 1. **Expand marketing efforts** in Q1 2025
757
- 2. **Invest in customer support** infrastructure
758
- 3. **Explore new markets** in Southeast Asia
759
-
760
- ---
761
-
762
- *Report generated on January 24, 2026*
763
- """,
764
- "page_break": False
765
  }
766
  ]
767
 
768
- success = generator.generate(
769
  sections=sections,
770
- filename="Q4_2024_Report.pdf",
771
  output_dir="./reports"
772
  )
773
 
774
  if success:
775
- print("✓ Report generated successfully!")
776
  else:
777
  print("✗ Report generation failed!")
 
1
  """
2
+ Professional Q4 Report Generator - Soft Professional Theme
3
+ Comfortable, professional colors with random filename generation
4
  """
5
  import markdown
6
  from weasyprint import HTML, CSS
7
  import requests
8
  import os
9
  import re
10
+ import secrets
11
  from datetime import datetime
12
  from typing import List, Dict, Optional
13
  import logging
 
24
  logger = logging.getLogger(__name__)
25
 
26
  # ---------------------------------------------------------
27
+ # SOFT PROFESSIONAL THEME - COMFORTABLE COLORS
28
  # ---------------------------------------------------------
29
  CSS_STYLE = """
30
  @page {
 
42
 
43
  body {
44
  font-family: 'Helvetica', Arial, sans-serif;
45
+ color: #2d3748;
46
  line-height: 1.65;
47
  font-size: 11pt;
48
  }
 
50
  .header {
51
  position: running(header);
52
  font-size: 9pt;
53
+ color: #718096;
54
+ border-bottom: 1px solid #e2e8f0;
55
  padding-bottom: 6px;
56
  margin-bottom: 20px;
57
  }
 
59
  .footer {
60
  position: running(footer);
61
  font-size: 9pt;
62
+ color: #718096;
63
+ border-top: 1px solid #e2e8f0;
64
  padding-top: 6px;
65
  text-align: center;
66
  }
 
70
  }
71
 
72
  .confidential-mark {
73
+ color: #e53e3e;
74
  font-weight: bold;
75
  float: left;
76
  }
 
84
  .cover-title {
85
  font-size: 36pt;
86
  font-weight: bold;
87
+ color: #4a5568;
88
  margin-bottom: 0.5in;
89
  letter-spacing: 1px;
90
  line-height: 1.2;
 
92
 
93
  .cover-subtitle {
94
  font-size: 20pt;
95
+ color: #5a67d8;
96
  margin-bottom: 1.2in;
97
  font-weight: 500;
98
  }
99
 
100
  .cover-meta {
101
  font-size: 15pt;
102
+ color: #718096;
103
  margin-bottom: 0.25in;
104
  line-height: 1.5;
105
  }
106
 
107
  .cover-date {
108
  font-size: 12pt;
109
+ color: #a0aec0;
110
  margin-top: 0.5in;
111
  }
112
 
113
  .cover-divider {
114
  width: 3in;
115
  height: 3px;
116
+ background: linear-gradient(to right, #667eea, #764ba2);
117
  margin: 0.5in auto;
118
  }
119
 
120
  h1 {
121
+ color: #4a5568;
122
  font-size: 20pt;
123
+ border-bottom: 3px solid #7c3aed;
124
  padding-bottom: 10px;
125
  margin-top: 28px;
126
  margin-bottom: 18px;
 
129
  }
130
 
131
  h2 {
132
+ color: #5a67d8;
133
  font-size: 15pt;
134
  margin-top: 22px;
135
  margin-bottom: 14px;
 
138
  }
139
 
140
  h3 {
141
+ color: #4c51bf;
142
  font-size: 13pt;
143
  margin-top: 18px;
144
  margin-bottom: 12px;
 
147
  }
148
 
149
  h4 {
150
+ color: #667eea;
151
  font-size: 12pt;
152
  margin-top: 16px;
153
  margin-bottom: 10px;
 
156
  }
157
 
158
  h5 {
159
+ color: #718096;
160
  font-size: 11.5pt;
161
  margin-top: 14px;
162
  margin-bottom: 8px;
 
165
  }
166
 
167
  h6 {
168
+ color: #a0aec0;
169
  font-size: 11pt;
170
  margin-top: 12px;
171
  margin-bottom: 8px;
 
184
  margin: 18px 0;
185
  font-size: 10pt;
186
  page-break-inside: avoid;
187
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
188
  }
189
 
190
  thead {
191
+ background: linear-gradient(to bottom, #667eea, #5a67d8);
192
  }
193
 
194
  th {
195
+ background-color: #667eea;
196
  color: white;
197
  padding: 12px;
198
  text-align: left;
199
  font-weight: bold;
200
+ border: 1px solid #5a67d8;
201
  font-size: 10.5pt;
202
  }
203
 
204
  td {
205
  padding: 10px 12px;
206
+ border: 1px solid #e2e8f0;
207
  line-height: 1.5;
208
  }
209
 
210
  tr:nth-child(even) {
211
+ background-color: #f7fafc;
212
  }
213
 
214
  tr:nth-child(odd) {
 
216
  }
217
 
218
  .highlight-box {
219
+ background-color: #edf2f7;
220
+ border-left: 5px solid #5a67d8;
221
  padding: 16px 20px;
222
  margin: 20px 0;
223
  page-break-inside: avoid;
224
+ box-shadow: 0 2px 4px rgba(0,0,0,0.04);
225
  }
226
 
227
  .highlight-box p:first-child {
 
233
  }
234
 
235
  .key-metric-box {
236
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
237
  color: white;
238
  border-radius: 8px;
239
  padding: 18px 22px;
240
  margin: 20px 0;
241
  page-break-inside: avoid;
242
+ box-shadow: 0 3px 6px rgba(0,0,0,0.12);
243
  }
244
 
245
  .key-metric-box h3,
 
262
  }
263
 
264
  blockquote {
265
+ border-left: 5px solid #5a67d8;
266
  margin-left: 0;
267
  padding-left: 20px;
268
+ color: #4a5568;
269
  font-style: italic;
270
+ background-color: #f7fafc;
271
  padding: 14px 14px 14px 20px;
272
  margin: 18px 0;
273
+ box-shadow: 0 1px 2px rgba(0,0,0,0.04);
274
  }
275
 
276
  blockquote p:first-child {
 
283
 
284
  blockquote strong {
285
  font-weight: 700;
286
+ color: #2d3748;
287
  }
288
 
289
  .figure-container {
 
295
  .figure-container img {
296
  max-width: 100%;
297
  height: auto;
298
+ border: 1px solid #e2e8f0;
299
  padding: 8px;
300
  background: white;
301
+ box-shadow: 0 2px 4px rgba(0,0,0,0.08);
302
  }
303
 
304
  .figure-caption {
305
  font-size: 9.5pt;
306
+ color: #718096;
307
  margin-top: 10px;
308
  font-style: italic;
309
  font-weight: 500;
310
  }
311
 
312
  .executive-summary {
313
+ background-color: #ebf4ff;
314
+ border: 2px solid #5a67d8;
315
  padding: 20px;
316
  margin: 24px 0;
317
  page-break-inside: avoid;
 
395
  /* TEXT FORMATTING */
396
  strong, b {
397
  font-weight: 700;
398
+ color: #2d3748;
399
  }
400
 
401
  em, i {
 
404
 
405
  code {
406
  font-family: 'Courier New', monospace;
407
+ background-color: #edf2f7;
408
  padding: 2px 6px;
409
  border-radius: 3px;
410
  font-size: 10pt;
411
+ color: #805ad5;
412
  }
413
 
414
  pre {
415
+ background-color: #edf2f7;
416
+ border: 1px solid #e2e8f0;
417
  border-radius: 4px;
418
  padding: 12px;
419
  overflow-x: auto;
 
426
  pre code {
427
  background-color: transparent;
428
  padding: 0;
429
+ color: #4a5568;
430
  }
431
 
432
  a {
433
+ color: #5a67d8;
434
  text-decoration: none;
435
  }
436
 
 
440
 
441
  hr {
442
  border: none;
443
+ border-top: 2px solid #e2e8f0;
444
  margin: 24px 0;
445
  }
446
  """
447
 
448
+ # ---------------------------------------------------------
449
+ # FILENAME GENERATION
450
+ # ---------------------------------------------------------
451
+ def generate_random_filename(base_name: str = "Q4_Report") -> str:
452
+ """Generate a random filename to avoid duplicates."""
453
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
454
+ random_suffix = secrets.token_hex(4) # 8 character hex string
455
+ return f"{base_name}_{timestamp}_{random_suffix}.pdf"
456
+
457
  # ---------------------------------------------------------
458
  # IMAGE HANDLING
459
  # ---------------------------------------------------------
460
  def fetch_image(url: str, output_dir: str) -> Optional[str]:
461
+ """Download image from URL to persistent file for PDF rendering."""
 
 
462
  if not url.startswith(('http://', 'https://')):
463
  if os.path.exists(url):
464
  abs_path = os.path.abspath(url)
 
507
  # MARKDOWN PREPROCESSING
508
  # ---------------------------------------------------------
509
  def preprocess_markdown(content: str) -> str:
510
+ """Preprocess markdown content to ensure proper parsing inside HTML blocks."""
 
 
 
 
511
  container_tags = ['div', 'blockquote', 'section', 'article', 'aside']
512
 
513
  for tag in container_tags:
 
 
514
  pattern = rf'<{tag}(\s+[^>]*?)?(?!\s+markdown\s*=)>'
515
 
516
  def add_markdown_attr(match):
517
  attrs = match.group(1) or ''
 
518
  if attrs:
519
  return f'<{tag}{attrs} markdown="1">'
520
  else:
 
528
  # Q4 REPORT GENERATOR CLASS
529
  # ---------------------------------------------------------
530
  class Q4ReportGenerator:
531
+ """Generates executive-quality Q4 reports with soft professional colors."""
532
 
533
  def __init__(self,
534
  title: str = "Q4 Performance Report",
 
575
  """
576
 
577
  def _process_section(self, section: Dict, output_dir: str) -> str:
578
+ """Process section with complete markdown parsing support."""
 
 
 
579
  html = ""
580
 
581
  content = section.get("content", "")
582
  if content:
 
583
  preprocessed_content = preprocess_markdown(content)
584
 
 
585
  html_content = markdown.markdown(
586
  preprocessed_content,
587
  extensions=[
588
+ 'extra', 'sane_lists', 'tables', 'fenced_code',
589
+ 'nl2br', 'codehilite', 'toc', 'md_in_html',
590
+ 'attr_list', 'def_list', 'abbr', 'footnotes', 'smarty'
 
 
 
 
 
 
 
 
 
 
591
  ],
592
  extension_configs={
593
  'codehilite': {
 
621
  </div>
622
  """
623
 
 
624
  if section.get("page_break", False):
625
  html += '<div class="page-break"></div>'
626
 
 
628
 
629
  def generate(self,
630
  sections: List[Dict],
631
+ filename: str = None,
632
  output_dir: str = "./output",
633
+ cover_page: bool = True) -> tuple[bool, str]:
634
+ """
635
+ Generate PDF report with random filename.
636
+ Returns: (success: bool, output_path: str)
637
+ """
638
  try:
639
  os.makedirs(output_dir, exist_ok=True)
640
+
641
+ # Generate random filename if not provided
642
+ if filename is None:
643
+ filename = generate_random_filename("Q4_Report")
644
+ elif not filename.endswith('.pdf'):
645
+ # Add random suffix even if filename provided
646
+ base = filename.replace('.pdf', '')
647
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
648
+ random_suffix = secrets.token_hex(4)
649
+ filename = f"{base}_{timestamp}_{random_suffix}.pdf"
650
+
651
  output_path = os.path.join(output_dir, filename)
652
 
653
  logger.info(f"Starting Q4 report generation: {output_path}")
654
+ logger.info("Theme: Soft Professional Colors")
655
 
656
  # Build HTML body
657
  body_html = ""
658
  if cover_page:
659
  body_html += self._build_cover_page()
660
 
 
661
  for idx, section in enumerate(sections):
662
  logger.info(f"Processing section {idx + 1}/{len(sections)}")
663
  body_html += self._process_section(section, output_dir)
 
686
  html_doc.write_pdf(output_path, stylesheets=[css_doc])
687
 
688
  logger.info(f"✓ PDF Generated Successfully: {output_path}")
689
+ return True, output_path
 
690
 
691
  except Exception as e:
692
  logger.error(f"✗ Error generating report: {e}", exc_info=True)
693
+ return False, ""
694
 
695
  def cleanup_images(self, output_dir: str):
696
  """Optional: Clean up downloaded images after PDF generation."""
 
708
  # EXAMPLE USAGE
709
  # ---------------------------------------------------------
710
  if __name__ == "__main__":
 
711
  generator = Q4ReportGenerator(
712
  title="Q4 2024 Performance Report",
713
  subtitle="Executive Summary & Analysis",
 
744
  </div>
745
  """,
746
  "page_break": False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
747
  }
748
  ]
749
 
750
+ success, output_path = generator.generate(
751
  sections=sections,
752
+ filename="Q4_2024_Report", # Will add random suffix automatically
753
  output_dir="./reports"
754
  )
755
 
756
  if success:
757
+ print(f"✓ Report generated: {output_path}")
758
  else:
759
  print("✗ Report generation failed!")