11import functools
22import io
33import base64
4+ import logging
45from typing import List , Optional
56
6- # Core imports
77import 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
1111try :
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+ )
2431except 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+ )
2635except 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
3140class 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 )
5260def 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 ('<' , '<' ).replace ('>' , '>' )
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 ("&" , "&" )
120+ .replace ("<" , "<" )
121+ .replace (">" , ">" )
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