ABDALLALSWAITI commited on
Commit
ce83640
·
verified ·
1 Parent(s): c804aef

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -145
app.py CHANGED
@@ -8,105 +8,61 @@ import shutil
8
  import base64
9
  import logging
10
  from typing import List, Optional
11
- from contextlib import asynccontextmanager
12
 
13
  # Configure logging
14
- logging.basicConfig(
15
- level=logging.INFO,
16
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
17
- )
18
  logger = logging.getLogger(__name__)
19
 
20
- # Global state
21
- app_state = {"ready": False, "error": None}
 
 
 
 
22
 
23
- @asynccontextmanager
24
- async def lifespan(app: FastAPI):
25
- """Startup and shutdown events"""
26
- logger.info("=" * 60)
 
 
 
27
  logger.info("HTML to PDF Converter API Starting...")
28
- logger.info("=" * 60)
29
  logger.info("Checking dependencies...")
30
-
31
  try:
32
- # Check Node.js
33
- node_version = subprocess.run(
34
- ['node', '--version'],
35
- capture_output=True,
36
- text=True,
37
- timeout=5
38
  )
39
- logger.info(f"Node.js version: {node_version.stdout.strip()}")
40
-
41
- # Check Chromium
42
- chromium_path = os.environ.get('PUPPETEER_EXECUTABLE_PATH', '/usr/bin/chromium')
 
 
 
 
43
  if os.path.exists(chromium_path):
44
  logger.info(f"Chromium found at: {chromium_path}")
45
  else:
46
- logger.error(f"Chromium NOT found at: {chromium_path}")
47
- app_state["error"] = f"Chromium not found at {chromium_path}"
48
-
49
- # Check Puppeteer script
50
- puppeteer_script = '/app/puppeteer_pdf.js'
51
- if os.path.exists(puppeteer_script):
52
  logger.info("Puppeteer script found")
53
  else:
54
- logger.error(f"Puppeteer script NOT found at {puppeteer_script}")
55
- app_state["error"] = f"Puppeteer script not found"
56
-
57
- # Test Puppeteer
58
- logger.info("Testing Puppeteer...")
59
- test_html = "<html><body><h1>Test</h1></body></html>"
60
- test_dir = tempfile.mkdtemp()
61
- try:
62
- test_file = os.path.join(test_dir, "test.html")
63
- with open(test_file, 'w') as f:
64
- f.write(test_html)
65
-
66
- result = subprocess.run(
67
- ['node', puppeteer_script, test_file, '9:16'],
68
- capture_output=True,
69
- text=True,
70
- timeout=30,
71
- cwd='/app'
72
- )
73
-
74
- if result.returncode == 0:
75
- logger.info("✓ Puppeteer test PASSED")
76
- app_state["ready"] = True
77
- else:
78
- logger.error(f"✗ Puppeteer test FAILED: {result.stderr}")
79
- app_state["error"] = f"Puppeteer test failed: {result.stderr}"
80
- except Exception as e:
81
- logger.error(f"✗ Puppeteer test FAILED with exception: {e}")
82
- app_state["error"] = str(e)
83
- finally:
84
- shutil.rmtree(test_dir, ignore_errors=True)
85
-
86
- logger.info("=" * 60)
87
- if app_state["ready"]:
88
- logger.info("✓ API is ready!")
89
- else:
90
- logger.error(f"✗ API NOT ready: {app_state.get('error', 'Unknown error')}")
91
- logger.info("=" * 60)
92
-
93
- except Exception as e:
94
- logger.error(f"Startup check failed: {e}")
95
- app_state["error"] = str(e)
96
-
97
- # THIS IS THE KEY: yield and keep the app running!
98
- yield
99
-
100
- # Shutdown (only runs when app is stopping)
101
- logger.info("Shutting down...")
102
 
103
- # Create FastAPI app with lifespan
104
- app = FastAPI(
105
- title="HTML to PDF Converter API",
106
- description="Convert HTML to PDF with page breaks and image support",
107
- version="1.0.0",
108
- lifespan=lifespan
109
- )
110
 
111
  # Add CORS middleware
112
  app.add_middleware(
@@ -121,9 +77,7 @@ app.add_middleware(
121
  async def root():
122
  """Root endpoint - returns immediately for health check"""
123
  return {
124
- "status": "online" if app_state["ready"] else "error",
125
- "ready": app_state["ready"],
126
- "error": app_state.get("error"),
127
  "service": "HTML to PDF Converter API",
128
  "version": "1.0.0",
129
  "docs": "/docs",
@@ -133,49 +87,37 @@ async def root():
133
  @app.get("/health")
134
  async def health():
135
  """Health check endpoint"""
136
- if not app_state["ready"]:
137
- raise HTTPException(503, f"Service not ready: {app_state.get('error', 'Unknown error')}")
138
- return {"status": "healthy", "ready": True}
139
 
140
  @app.get("/ui", response_class=HTMLResponse)
141
  async def ui():
142
  """UI documentation page"""
143
- status_class = "online" if app_state["ready"] else "error"
144
- status_text = "✓ Online" if app_state["ready"] else "✗ Error"
145
- error_html = ""
146
- if not app_state["ready"]:
147
- error_html = f"""
148
- <div style="background: #fee; border: 2px solid #c00; padding: 15px; margin: 20px 0; border-radius: 5px;">
149
- <strong>⚠️ Service Error:</strong> {app_state.get('error', 'Unknown error')}
150
- </div>
151
- """
152
-
153
- return f"""
154
  <!DOCTYPE html>
155
  <html>
156
  <head>
157
  <title>HTML to PDF API</title>
158
  <meta name="viewport" content="width=device-width, initial-scale=1">
159
  <style>
160
- * {{ margin: 0; padding: 0; box-sizing: border-box; }}
161
- body {{
162
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
163
  line-height: 1.6;
164
  color: #333;
165
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
166
  min-height: 100vh;
167
  padding: 20px;
168
- }}
169
- .container {{
170
  max-width: 900px;
171
  margin: 0 auto;
172
  background: white;
173
  border-radius: 10px;
174
  padding: 40px;
175
  box-shadow: 0 10px 40px rgba(0,0,0,0.2);
176
- }}
177
- h1 {{ color: #667eea; margin-bottom: 10px; font-size: 2.5em; }}
178
- .status {{
179
  display: inline-block;
180
  background: #10b981;
181
  color: white;
@@ -183,29 +125,34 @@ async def ui():
183
  border-radius: 20px;
184
  font-size: 0.9em;
185
  margin-bottom: 30px;
186
- }}
187
- .status.error {{
188
- background: #ef4444;
189
- }}
190
- h2 {{ color: #333; margin: 30px 0 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }}
191
- .endpoint {{
192
  background: #f8f9fa;
193
  border-left: 4px solid #667eea;
194
  padding: 20px;
195
  margin: 15px 0;
196
  border-radius: 5px;
197
- }}
198
- .endpoint h3 {{ color: #667eea; margin-bottom: 10px; }}
199
- code {{
200
  background: #e9ecef;
201
  padding: 3px 8px;
202
  border-radius: 4px;
203
  font-family: 'Courier New', monospace;
204
  font-size: 0.9em;
205
- }}
206
- ul {{ margin-left: 20px; }}
207
- li {{ margin: 8px 0; }}
208
- .btn {{
 
 
 
 
 
 
 
 
209
  display: inline-block;
210
  background: #667eea;
211
  color: white;
@@ -214,20 +161,18 @@ async def ui():
214
  text-decoration: none;
215
  margin: 5px;
216
  transition: all 0.3s;
217
- }}
218
- .btn:hover {{
219
  background: #5568d3;
220
  transform: translateY(-2px);
221
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
222
- }}
223
  </style>
224
  </head>
225
  <body>
226
  <div class="container">
227
  <h1>📄 HTML to PDF Converter API</h1>
228
- <span class="status {status_class}">{status_text}</span>
229
-
230
- {error_html}
231
 
232
  <p style="margin: 20px 0; font-size: 1.1em;">
233
  A powerful API to convert HTML documents to PDF with support for
@@ -313,9 +258,6 @@ def inject_page_css(html: str, ratio: str) -> str:
313
 
314
  def convert_to_pdf(html: str, ratio: str) -> bytes:
315
  """Convert HTML to PDF using Puppeteer"""
316
- if not app_state["ready"]:
317
- raise HTTPException(503, f"Service not ready: {app_state.get('error', 'Unknown')}")
318
-
319
  temp_dir = tempfile.mkdtemp()
320
  try:
321
  html = inject_page_css(html, ratio)
@@ -330,18 +272,18 @@ def convert_to_pdf(html: str, ratio: str) -> bytes:
330
  ['node', '/app/puppeteer_pdf.js', html_file, ratio],
331
  capture_output=True,
332
  text=True,
333
- timeout=50, # Reduced from 60 to give buffer
334
  cwd='/app'
335
  )
336
 
337
  if result.returncode != 0:
338
  logger.error(f"Conversion failed: {result.stderr}")
339
- raise HTTPException(500, f"PDF conversion failed: {result.stderr}")
340
 
341
  pdf_file = html_file.replace('.html', '.pdf')
342
 
343
  if not os.path.exists(pdf_file):
344
- raise HTTPException(500, "PDF file was not generated")
345
 
346
  with open(pdf_file, 'rb') as f:
347
  pdf_bytes = f.read()
@@ -349,12 +291,6 @@ def convert_to_pdf(html: str, ratio: str) -> bytes:
349
  logger.info(f"PDF generated: {len(pdf_bytes)} bytes")
350
  return pdf_bytes
351
 
352
- except subprocess.TimeoutExpired:
353
- logger.error("PDF conversion timed out")
354
- raise HTTPException(504, "PDF conversion timed out. Try a simpler HTML document.")
355
- except Exception as e:
356
- logger.error(f"Conversion error: {str(e)}")
357
- raise HTTPException(500, str(e))
358
  finally:
359
  shutil.rmtree(temp_dir, ignore_errors=True)
360
 
@@ -398,8 +334,6 @@ async def convert(
398
  }
399
  )
400
 
401
- except HTTPException:
402
- raise
403
  except Exception as e:
404
  logger.error(f"Error: {str(e)}")
405
  raise HTTPException(500, str(e))
@@ -431,8 +365,6 @@ async def convert_html(
431
  }
432
  )
433
 
434
- except HTTPException:
435
- raise
436
  except Exception as e:
437
  logger.error(f"Error: {str(e)}")
438
  raise HTTPException(500, str(e))
@@ -463,8 +395,6 @@ async def convert_base64(
463
  "size_bytes": len(pdf)
464
  })
465
 
466
- except HTTPException:
467
- raise
468
  except Exception as e:
469
  logger.error(f"Error: {str(e)}")
470
  raise HTTPException(500, str(e))
 
8
  import base64
9
  import logging
10
  from typing import List, Optional
11
+ import asyncio # Required for async subprocess
12
 
13
  # Configure logging
14
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
 
 
15
  logger = logging.getLogger(__name__)
16
 
17
+ # Create FastAPI app
18
+ app = FastAPI(
19
+ title="HTML to PDF Converter API",
20
+ description="Convert HTML to PDF with page breaks and image support",
21
+ version="1.0.0"
22
+ )
23
 
24
+ # --- START: Corrected Startup Logic ---
25
+ @app.on_event("startup")
26
+ async def startup_event():
27
+ """
28
+ Run startup checks. This code runs only once when the server starts.
29
+ """
30
+ logger.info("============================================================")
31
  logger.info("HTML to PDF Converter API Starting...")
32
+ logger.info("============================================================")
33
  logger.info("Checking dependencies...")
 
34
  try:
35
+ # Check Node.js version asynchronously
36
+ proc = await asyncio.create_subprocess_shell(
37
+ "node --version",
38
+ stdout=asyncio.subprocess.PIPE,
39
+ stderr=asyncio.subprocess.PIPE
 
40
  )
41
+ stdout, stderr = await proc.communicate()
42
+ if proc.returncode == 0:
43
+ logger.info(f"Node.js version: {stdout.decode().strip()}")
44
+ else:
45
+ logger.error(f"Could not get Node.js version: {stderr.decode().strip()}")
46
+
47
+ # Check for Chromium executable
48
+ chromium_path = os.getenv("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium")
49
  if os.path.exists(chromium_path):
50
  logger.info(f"Chromium found at: {chromium_path}")
51
  else:
52
+ logger.error(f"Chromium executable not found at {chromium_path}!")
53
+
54
+ # Check for Puppeteer script
55
+ if os.path.exists("/app/puppeteer_pdf.js"):
 
 
56
  logger.info("Puppeteer script found")
57
  else:
58
+ logger.error("puppeteer_pdf.js not found in /app directory!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
+ logger.info("============================================================")
61
+ logger.info("API is ready!")
62
+ logger.info("============================================================")
63
+ except Exception as e:
64
+ logger.critical(f"A critical error occurred during startup: {e}")
65
+ # --- END: Corrected Startup Logic ---
 
66
 
67
  # Add CORS middleware
68
  app.add_middleware(
 
77
  async def root():
78
  """Root endpoint - returns immediately for health check"""
79
  return {
80
+ "status": "online",
 
 
81
  "service": "HTML to PDF Converter API",
82
  "version": "1.0.0",
83
  "docs": "/docs",
 
87
  @app.get("/health")
88
  async def health():
89
  """Health check endpoint"""
90
+ return {"status": "healthy"}
 
 
91
 
92
  @app.get("/ui", response_class=HTMLResponse)
93
  async def ui():
94
  """UI documentation page"""
95
+ return """
 
 
 
 
 
 
 
 
 
 
96
  <!DOCTYPE html>
97
  <html>
98
  <head>
99
  <title>HTML to PDF API</title>
100
  <meta name="viewport" content="width=device-width, initial-scale=1">
101
  <style>
102
+ * { margin: 0; padding: 0; box-sizing: border-box; }
103
+ body {
104
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
105
  line-height: 1.6;
106
  color: #333;
107
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
108
  min-height: 100vh;
109
  padding: 20px;
110
+ }
111
+ .container {
112
  max-width: 900px;
113
  margin: 0 auto;
114
  background: white;
115
  border-radius: 10px;
116
  padding: 40px;
117
  box-shadow: 0 10px 40px rgba(0,0,0,0.2);
118
+ }
119
+ h1 { color: #667eea; margin-bottom: 10px; font-size: 2.5em; }
120
+ .status {
121
  display: inline-block;
122
  background: #10b981;
123
  color: white;
 
125
  border-radius: 20px;
126
  font-size: 0.9em;
127
  margin-bottom: 30px;
128
+ }
129
+ h2 { color: #333; margin: 30px 0 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
130
+ .endpoint {
 
 
 
131
  background: #f8f9fa;
132
  border-left: 4px solid #667eea;
133
  padding: 20px;
134
  margin: 15px 0;
135
  border-radius: 5px;
136
+ }
137
+ .endpoint h3 { color: #667eea; margin-bottom: 10px; }
138
+ code {
139
  background: #e9ecef;
140
  padding: 3px 8px;
141
  border-radius: 4px;
142
  font-family: 'Courier New', monospace;
143
  font-size: 0.9em;
144
+ }
145
+ pre {
146
+ background: #2d3748;
147
+ color: #fff;
148
+ padding: 15px;
149
+ border-radius: 5px;
150
+ overflow-x: auto;
151
+ margin: 10px 0;
152
+ }
153
+ ul { margin-left: 20px; }
154
+ li { margin: 8px 0; }
155
+ .btn {
156
  display: inline-block;
157
  background: #667eea;
158
  color: white;
 
161
  text-decoration: none;
162
  margin: 5px;
163
  transition: all 0.3s;
164
+ }
165
+ .btn:hover {
166
  background: #5568d3;
167
  transform: translateY(-2px);
168
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
169
+ }
170
  </style>
171
  </head>
172
  <body>
173
  <div class="container">
174
  <h1>📄 HTML to PDF Converter API</h1>
175
+ <span class="status">✓ Online</span>
 
 
176
 
177
  <p style="margin: 20px 0; font-size: 1.1em;">
178
  A powerful API to convert HTML documents to PDF with support for
 
258
 
259
  def convert_to_pdf(html: str, ratio: str) -> bytes:
260
  """Convert HTML to PDF using Puppeteer"""
 
 
 
261
  temp_dir = tempfile.mkdtemp()
262
  try:
263
  html = inject_page_css(html, ratio)
 
272
  ['node', '/app/puppeteer_pdf.js', html_file, ratio],
273
  capture_output=True,
274
  text=True,
275
+ timeout=60,
276
  cwd='/app'
277
  )
278
 
279
  if result.returncode != 0:
280
  logger.error(f"Conversion failed: {result.stderr}")
281
+ raise Exception(f"PDF conversion failed: {result.stderr}")
282
 
283
  pdf_file = html_file.replace('.html', '.pdf')
284
 
285
  if not os.path.exists(pdf_file):
286
+ raise Exception("PDF file was not generated")
287
 
288
  with open(pdf_file, 'rb') as f:
289
  pdf_bytes = f.read()
 
291
  logger.info(f"PDF generated: {len(pdf_bytes)} bytes")
292
  return pdf_bytes
293
 
 
 
 
 
 
 
294
  finally:
295
  shutil.rmtree(temp_dir, ignore_errors=True)
296
 
 
334
  }
335
  )
336
 
 
 
337
  except Exception as e:
338
  logger.error(f"Error: {str(e)}")
339
  raise HTTPException(500, str(e))
 
365
  }
366
  )
367
 
 
 
368
  except Exception as e:
369
  logger.error(f"Error: {str(e)}")
370
  raise HTTPException(500, str(e))
 
395
  "size_bytes": len(pdf)
396
  })
397
 
 
 
398
  except Exception as e:
399
  logger.error(f"Error: {str(e)}")
400
  raise HTTPException(500, str(e))