Skip to content

Commit c60af79

Browse files
committed
chore: save updates to pdf_builder_node.py
1 parent 6985263 commit c60af79

File tree

1 file changed

+89
-69
lines changed

1 file changed

+89
-69
lines changed

api/nodes/pdf_builder_node.py

Lines changed: 89 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,41 @@
11
import functools
22
import io
33
import base64
4+
import logging
45
from typing import List, Optional
56

6-
# Core imports
77
import httpx
8-
import logging
8+
from weasyprint import HTML
99

10-
# Monkey-patch PyDyf PDF constructor to accept version and identifier args and set version attribute
10+
# Monkey-patch PyDyf PDF constructor to accept version and identifier args
1111
try:
1212
import pydyf
13+
1314
_orig_pdf_init = pydyf.PDF.__init__
14-
def _patched_pdf_init(self, version=b'1.7', identifier=None, *args, **kwargs):
15-
# Store PDF version for compatibility
15+
16+
def _patched_pdf_init(self, version: bytes = b"1.7", identifier=None, *args, **kwargs):
1617
try:
17-
self.version = version if isinstance(version, (bytes, bytearray)) else str(version).encode()
18+
self.version = (
19+
version
20+
if isinstance(version, (bytes, bytearray))
21+
else str(version).encode()
22+
)
1823
except Exception:
19-
self.version = b'1.7'
20-
# Call original initializer
24+
self.version = b"1.7"
2125
_orig_pdf_init(self)
26+
2227
pydyf.PDF.__init__ = _patched_pdf_init
23-
logging.getLogger(__name__).info("Patched pydyf.PDF.__init__ to accept version and identifier args")
28+
logging.getLogger(__name__).info(
29+
"Patched pydyf.PDF.__init__ to accept version and identifier args"
30+
)
2431
except ImportError:
25-
logging.getLogger(__name__).warning("pydyf not installed; cannot patch PDF constructor")
32+
logging.getLogger(__name__).warning(
33+
"pydyf not installed; cannot patch PDF constructor"
34+
)
2635
except Exception as e:
2736
logging.getLogger(__name__).error(f"Error patching pydyf.PDF.__init__: {e}")
2837

29-
from weasyprint import HTML
38+
3039
# Node decorator with retry logic
3140
class Node:
3241
def __init__(self, retries: int = 0):
@@ -36,96 +45,107 @@ def __call__(self, fn):
3645
@functools.wraps(fn)
3746
def wrapper(*args, **kwargs):
3847
last_exc = None
39-
for _ in range(self.retries):
48+
for _ in range(self.retries + 1):
4049
try:
4150
return fn(*args, **kwargs)
4251
except Exception as e:
4352
last_exc = e
44-
if last_exc is not None:
45-
raise last_exc
46-
return fn(*args, **kwargs)
53+
raise last_exc
4754

4855
wrapper.__wrapped__ = fn
4956
return wrapper
5057

58+
5159
@Node(retries=0)
5260
def PdfBuilderNode(
5361
logo_url: Optional[str],
5462
palette: List[str],
55-
prompts_by_cat: dict[str, list[str]]
63+
prompts_by_cat: dict[str, list[str]],
5664
) -> bytes:
5765
"""
58-
Build a branded PDF containing prompts and usage tips.
59-
Embeds logo if available and applies color palette to headings.
66+
Build a branded PDF containing AI shortcuts (prompts).
67+
Embeds logo if available, applies brand colors, and adds footer.
6068
Returns raw PDF bytes.
6169
"""
70+
logger = logging.getLogger(__name__)
71+
6272
# Fetch and embed logo as data URI
6373
img_data = None
6474
if logo_url:
6575
try:
6676
resp = httpx.get(logo_url, timeout=10.0)
6777
resp.raise_for_status()
68-
ct = resp.headers.get("Content-Type", "image/png")
78+
content_type = resp.headers.get("Content-Type", "image/png")
6979
b64 = base64.b64encode(resp.content).decode()
70-
img_data = f"data:{ct};base64,{b64}"
80+
img_data = f"data:{content_type};base64,{b64}"
7181
except Exception:
7282
img_data = None
7383

74-
# Determine primary and accent colors
75-
primary = palette[0] if len(palette) > 0 else "#000000"
84+
# Determine colors
85+
primary = palette[0] if palette else "#1c1c1c"
7686
accent = palette[1] if len(palette) > 1 else primary
7787

78-
# Build HTML content with grouped prompts
79-
html = f"""<!DOCTYPE html>
80-
<html>
81-
<head>
82-
<meta charset=\"utf-8\">
83-
<title>AI Prompt Pack</title>
84-
<style>
85-
body {{ font-family: Arial, sans-serif; margin: 2em; }}
86-
.header {{ display: flex; align-items: center; border-bottom: 2px solid {primary}; padding-bottom: 1em; margin-bottom: 2em; }}
87-
.header img {{ max-height: 50px; margin-right: 1em; }}
88-
.header h1 {{ color: {primary}; margin: 0; }}
89-
ol {{ padding-left: 1em; }}
90-
li {{ margin-bottom: 1.5em; }}
91-
.prompt {{ font-size: 1.1em; color: #333; white-space: pre-wrap; }}
92-
.tip {{ font-size: 0.9em; color: {accent}; margin-top: 0.5em; }}
93-
</style>
94-
</head>
95-
<body>
96-
<div class=\"header\">"""
88+
# Build HTML
89+
html_parts = [
90+
"<!DOCTYPE html>",
91+
"<html><head><meta charset='utf-8'>",
92+
"<title>AI Shortcuts Kit</title>",
93+
"<style>",
94+
" body { font-family: Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #1c1c1c; margin: 2em; }",
95+
f" .header {{ display: flex; align-items: center; border-bottom: 2px solid {primary}; padding-bottom: 1em; margin-bottom: 2em; }}",
96+
" .header img { max-height: 50px; margin-right: 1em; }",
97+
f" .header h1 {{ color: {primary}; margin: 0; font-size: 1.75rem; }}",
98+
" section h2 { font-size: 1.25rem; margin-top: 1.5em; color: #333; }",
99+
" ol { padding-left: 1.25em; }",
100+
" li.prompt { margin-bottom: 1.5em; }",
101+
" .prompt { white-space: pre-wrap; font-size: 1rem; color: #333; }",
102+
f" footer {{ margin-top: 3em; border-top: 1px solid {accent}; padding-top: 1em; font-size: 0.875rem; color: #555; text-align: center; }}",
103+
" footer a { color: inherit; text-decoration: none; }",
104+
"</style></head><body>",
105+
]
106+
107+
# Header
108+
header = "<div class='header'>"
97109
if img_data:
98-
html += f"<img src=\"{img_data}\" alt=\"logo\"/>"
99-
html += f"<h1>AI Prompt Pack</h1></div>" # header end
100-
# Render grouped prompts by category
101-
for category, items in prompts_by_cat.items():
102-
html += f"<section><h2>{category}</h2><ol>"
103-
for prmpt in items:
104-
safe_prompt = prmpt.replace('<', '&lt;').replace('>', '&gt;')
105-
html += f"<li class=\"prompt\">{safe_prompt}</li>"
106-
html += "</ol></section>"
107-
html += "</body></html>"
108-
109-
# Generate PDF, with detailed logging on failure
110+
header += f"<img src='{img_data}' alt='Fyne LLC logo'/>"
111+
header += "<h1>AI Shortcuts Kit</h1></div>"
112+
html_parts.append(header)
113+
114+
# Prompts by category
115+
for category, prompts in prompts_by_cat.items():
116+
html_parts.append(f"<section><h2>{category}</h2><ol>")
117+
for prompt in prompts:
118+
escaped = (
119+
prompt.replace("&", "&amp;")
120+
.replace("<", "&lt;")
121+
.replace(">", "&gt;")
122+
)
123+
html_parts.append(f"<li class='prompt'>{escaped}</li>")
124+
html_parts.append("</ol></section>")
125+
126+
# Footer
127+
html_parts.append(
128+
"<footer>"
129+
"© 2025 FYNE LLC. "
130+
"<a href='https://bizassistant.fyne-llc.com' target='_blank'>bizassistant.fyne-llc.com</a>"
131+
"</footer>"
132+
)
133+
134+
html_parts.append("</body></html>")
135+
html_content = "\n".join(html_parts)
136+
137+
# Generate PDF
110138
try:
111-
# Log WeasyPrint/PyDyf versions for debugging
112-
try:
113-
import pkg_resources
114-
wp_ver = pkg_resources.get_distribution('weasyprint').version
115-
pd_ver = pkg_resources.get_distribution('pydyf').version
116-
logging.getLogger(__name__).info(
117-
f"PdfBuilderNode using WeasyPrint {wp_ver}, PyDyf {pd_ver}")
118-
except Exception:
119-
pass
120-
pdf_bytes = HTML(string=html).write_pdf()
139+
pdf_bytes = HTML(string=html_content).write_pdf()
140+
logger.info("PdfBuilderNode generated PDF, size=%d bytes", len(pdf_bytes))
121141
return pdf_bytes
122142
except Exception as e:
123-
logger = logging.getLogger(__name__)
124-
# Log context: counts and html size
125143
logger.error(
126-
f"PdfBuilderNode error: logo_url={logo_url}, "
127-
f"palette={palette}, prompts={len(prompts)}, tips={len(tips)}, html_length={len(html)}"
144+
"PdfBuilderNode error: logo_url=%s, palette=%s, categories=%d, html_length=%d",
145+
logo_url,
146+
palette,
147+
len(prompts_by_cat),
148+
len(html_content),
128149
)
129-
logger.exception("Exception stack trace:")
130-
# Re-raise to be caught by FastAPI and returned as HTTP 500
150+
logger.exception("Exception during PDF generation")
131151
raise

0 commit comments

Comments
 (0)