simoncck commited on
Commit
96ca54f
·
verified ·
1 Parent(s): 03e25b0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +107 -175
app.py CHANGED
@@ -9,26 +9,18 @@ from datetime import datetime
9
  from io import BytesIO
10
  from typing import Dict, List, Optional, Any
11
  from contextlib import asynccontextmanager
 
12
 
13
- import gradio as gr
14
  import uvicorn
15
  from fastapi import FastAPI, HTTPException, BackgroundTasks
16
  from fastapi.middleware.cors import CORSMiddleware
17
  from fastapi.responses import JSONResponse
18
  from fastapi.responses import FileResponse, HTMLResponse
19
- from fastapi.staticfiles import StaticFiles
20
- from pathlib import Path
21
  from pydantic import BaseModel
22
  from playwright.async_api import async_playwright, Browser, BrowserContext, Page
23
  from selenium import webdriver
24
  from selenium.webdriver.chrome.options import Options
25
- from selenium.webdriver.chrome.service import Service
26
- from selenium.webdriver.common.by import By
27
- from selenium.webdriver.support.ui import WebDriverWait
28
- from selenium.webdriver.support import expected_conditions as EC
29
- from selenium.common.exceptions import TimeoutException, WebDriverException
30
  from bs4 import BeautifulSoup
31
- from PIL import Image
32
 
33
  # Configure logging
34
  logging.basicConfig(level=logging.INFO)
@@ -103,8 +95,6 @@ app = FastAPI(
103
  )
104
 
105
  # ---------- serve the single-page UI at “/” ----------
106
- #BASE_DIR = pathlib.Path(__file__).resolve().parent
107
- #UI_FILE = BASE_DIR / "browser_automation_ui.html" # adjust if you stored it elsewhere
108
  BASE_DIR = Path(__file__).resolve().parent
109
  UI_FILE = BASE_DIR / "browser_automation_ui.html"
110
 
@@ -126,17 +116,16 @@ app.add_middleware(
126
  )
127
 
128
  # Utility functions
129
- def get_chrome_options():
130
- """Get Chrome options for Selenium"""
131
- options = Options()
132
- options.add_argument('--headless')
133
- options.add_argument('--no-sandbox')
134
- options.add_argument('--disable-dev-shm-usage')
135
- options.add_argument('--disable-gpu')
136
- options.add_argument('--window-size=1920,1080')
137
- return options
138
 
139
- def cleanup_old_sessions():
140
  """Clean up sessions older than 1 hour"""
141
  current_time = time.time()
142
  expired_sessions = []
@@ -148,191 +137,159 @@ def cleanup_old_sessions():
148
  for session_id in expired_sessions:
149
  asyncio.create_task(close_browser_session(session_id))
150
 
151
- async def close_browser_session(session_id: str):
152
- """Close a specific browser session"""
153
- if session_id in browser_instances:
154
- session = browser_instances[session_id]
155
-
156
- # Close Playwright session
157
- if 'playwright_page' in session:
158
- try:
159
- await session['playwright_page'].close()
160
- await session['playwright_context'].close()
161
- except Exception as e:
162
- logger.error(f"Error closing Playwright session {session_id}: {e}")
163
-
164
- # Close Selenium session
165
- if 'selenium_driver' in session:
166
- try:
167
- session['selenium_driver'].quit()
168
- except Exception as e:
169
- logger.error(f"Error closing Selenium session {session_id}: {e}")
170
-
171
- del browser_instances[session_id]
172
- logger.info(f"Closed browser session: {session_id}")
173
 
174
  # API Endpoints
175
 
176
  @app.get("/health")
177
- async def health_check():
178
- """Health check endpoint"""
179
- return {"status": "healthy", "timestamp": datetime.now().isoformat()}
180
 
181
  @app.post("/api/browser/launch")
182
- async def launch_browser(request: BrowserLaunchRequest):
183
  """Launch a new browser instance"""
184
  session_id = str(uuid.uuid4())
185
 
186
  try:
187
  # Launch Playwright browser
188
- context = await browser_pool.new_context(
189
  viewport={'width': request.width, 'height': request.height},
190
  user_agent=request.user_agent
191
  )
192
- page = await context.new_page()
193
-
194
- # Launch Selenium browser
195
- chrome_options = get_chrome_options()
196
- if request.user_agent:
197
- chrome_options.add_argument(f'--user-agent={request.user_agent}')
198
-
199
- selenium_driver = webdriver.Chrome(options=chrome_options)
200
- selenium_driver.set_window_size(request.width, request.height)
201
 
202
  # Store session
203
  browser_instances[session_id] = {
204
- 'playwright_context': context,
205
- 'playwright_page': page,
206
- 'selenium_driver': selenium_driver,
207
- 'created_at': time.time(),
208
- 'config': request.dict()
209
  }
210
-
211
- logger.info(f"Launched browser session: {session_id}")
212
- return {"session_id": session_id, "status": "launched"}
213
-
214
  except Exception as e:
215
  logger.error(f"Error launching browser: {e}")
216
  raise HTTPException(status_code=500, detail=str(e))
217
 
218
  @app.post("/api/browser/navigate")
219
- async def navigate_to_url(request: NavigateRequest):
220
  """Navigate to a URL"""
221
- if request.session_id not in browser_instances:
222
  raise HTTPException(status_code=404, detail="Session not found")
223
-
224
- session = browser_instances[request.session_id]
225
-
226
  try:
227
  # Navigate with Playwright
228
- await session['playwright_page'].goto(request.url, wait_until=request.wait_until)
229
-
230
- # Navigate with Selenium
231
- session['selenium_driver'].get(request.url)
232
-
233
- return {"status": "navigated", "url": request.url}
234
-
235
  except Exception as e:
236
- logger.error(f"Error navigating to {request.url}: {e}")
237
  raise HTTPException(status_code=500, detail=str(e))
238
 
239
  @app.post("/api/browser/screenshot")
240
- async def take_screenshot(request: ScreenshotRequest):
241
  """Take a screenshot"""
242
- if request.session_id not in browser_instances:
243
  raise HTTPException(status_code=404, detail="Session not found")
244
-
245
- session = browser_instances[request.session_id]
246
 
247
  try:
248
- if request.selector:
249
- # Screenshot specific element with Playwright
250
- element = await session['playwright_page'].locator(request.selector).first
251
- screenshot_bytes = await element.screenshot()
252
  else:
253
  # Full page screenshot with Playwright
254
- screenshot_bytes = await session['playwright_page'].screenshot(
255
- full_page=request.full_page
256
- )
257
-
258
  # Convert to base64
259
- screenshot_b64 = base64.b64encode(screenshot_bytes).decode()
260
-
261
- return {
262
- "screenshot": screenshot_b64,
263
- "format": "png",
264
- "timestamp": datetime.now().isoformat()
265
- }
266
-
267
  except Exception as e:
268
  logger.error(f"Error taking screenshot: {e}")
269
  raise HTTPException(status_code=500, detail=str(e))
270
 
 
 
 
 
 
 
 
 
 
 
 
271
  @app.post("/api/elements/action")
272
- async def perform_element_action(request: ElementActionRequest):
273
  """Perform action on an element"""
274
- if request.session_id not in browser_instances:
275
  raise HTTPException(status_code=404, detail="Session not found")
276
-
277
- session = browser_instances[request.session_id]
 
278
 
279
  try:
280
- page = session['playwright_page']
281
- element = page.locator(request.selector).first
282
-
283
- if request.action == "click":
284
- await element.click()
285
- elif request.action == "type":
286
- await element.fill(request.value or "")
287
- elif request.action == "scroll":
288
- await element.scroll_into_view_if_needed()
289
- elif request.action == "hover":
290
- await element.hover()
291
  else:
292
- raise HTTPException(status_code=400, detail="Invalid action")
293
-
294
- return {"status": "completed", "action": request.action}
295
-
296
  except Exception as e:
297
- logger.error(f"Error performing action {request.action}: {e}")
298
  raise HTTPException(status_code=500, detail=str(e))
299
 
300
  @app.get("/api/elements/inspect/{session_id}")
301
- async def inspect_page_elements(session_id: str):
302
  """Get all interactive elements on the page"""
303
  if session_id not in browser_instances:
304
  raise HTTPException(status_code=404, detail="Session not found")
305
-
306
- session = browser_instances[session_id]
 
 
 
 
 
307
 
308
  try:
309
- page = session['playwright_page']
310
-
311
- # Get page content
312
- content = await page.content()
313
- soup = BeautifulSoup(content, 'html.parser')
314
-
315
- # Find interactive elements
316
- interactive_selectors = [
317
- 'a', 'button', 'input', 'select', 'textarea',
318
- '[onclick]', '[href]', '[role="button"]'
319
- ]
320
-
321
- elements = []
322
- for selector in interactive_selectors:
323
- found_elements = soup.select(selector)
324
- for i, elem in enumerate(found_elements):
325
- element_info = {
326
- 'tag': elem.name,
327
- 'selector': f"{selector}:nth-of-type({i+1})",
328
- 'text': elem.get_text(strip=True)[:100],
329
- 'attributes': dict(elem.attrs),
330
- 'type': elem.get('type', 'N/A')
331
- }
332
- elements.append(element_info)
333
-
334
- return {"elements": elements, "total_count": len(elements)}
335
-
336
  except Exception as e:
337
  logger.error(f"Error inspecting elements: {e}")
338
  raise HTTPException(status_code=500, detail=str(e))
@@ -374,15 +331,6 @@ async def scrape_content(request: ScrapeRequest):
374
  logger.error(f"Error scraping content: {e}")
375
  raise HTTPException(status_code=500, detail=str(e))
376
 
377
- @app.delete("/api/browser/close/{session_id}")
378
- async def close_browser(session_id: str):
379
- """Close a browser session"""
380
- if session_id not in browser_instances:
381
- raise HTTPException(status_code=404, detail="Session not found")
382
-
383
- await close_browser_session(session_id)
384
- return {"status": "closed", "session_id": session_id}
385
-
386
  @app.get("/api/sessions")
387
  async def list_sessions():
388
  """List all active browser sessions"""
@@ -396,21 +344,5 @@ async def list_sessions():
396
 
397
  return {"sessions": sessions, "total_count": len(sessions)}
398
 
399
- # Background task to cleanup old sessions
400
- @app.on_event("startup")
401
- async def startup_event():
402
- async def cleanup_task():
403
- while True:
404
- cleanup_old_sessions()
405
- await asyncio.sleep(300) # Clean up every 5 minutes
406
-
407
- asyncio.create_task(cleanup_task())
408
-
409
  if __name__ == "__main__":
410
- uvicorn.run(
411
- "app:app",
412
- host="0.0.0.0",
413
- port=7860,
414
- reload=False,
415
- workers=1
416
- )
 
9
  from io import BytesIO
10
  from typing import Dict, List, Optional, Any
11
  from contextlib import asynccontextmanager
12
+ from pathlib import Path
13
 
 
14
  import uvicorn
15
  from fastapi import FastAPI, HTTPException, BackgroundTasks
16
  from fastapi.middleware.cors import CORSMiddleware
17
  from fastapi.responses import JSONResponse
18
  from fastapi.responses import FileResponse, HTMLResponse
 
 
19
  from pydantic import BaseModel
20
  from playwright.async_api import async_playwright, Browser, BrowserContext, Page
21
  from selenium import webdriver
22
  from selenium.webdriver.chrome.options import Options
 
 
 
 
 
23
  from bs4 import BeautifulSoup
 
24
 
25
  # Configure logging
26
  logging.basicConfig(level=logging.INFO)
 
95
  )
96
 
97
  # ---------- serve the single-page UI at “/” ----------
 
 
98
  BASE_DIR = Path(__file__).resolve().parent
99
  UI_FILE = BASE_DIR / "browser_automation_ui.html"
100
 
 
116
  )
117
 
118
  # Utility functions
119
+ def _chrome_opts():
120
+ o = Options()
121
+ o.add_argument("--headless")
122
+ o.add_argument("--no-sandbox")
123
+ o.add_argument("--disable-dev-shm-usage")
124
+ o.add_argument("--disable-gpu")
125
+ o.add_argument("--window-size=1920,1080")
126
+ return o
 
127
 
128
+ def _purge_idle():
129
  """Clean up sessions older than 1 hour"""
130
  current_time = time.time()
131
  expired_sessions = []
 
137
  for session_id in expired_sessions:
138
  asyncio.create_task(close_browser_session(session_id))
139
 
140
+ async def _close_session(session_id: str):
141
+ if session_id not in browser_instances:
142
+ return
143
+ sess = browser_instances[session_id]
144
+ # Playwright
145
+ try:
146
+ await sess['playwright_page'].close()
147
+ await sess['playwright_context'].close()
148
+ except Exception:
149
+ pass
150
+
151
+ del browser_instances[session_id]
152
+ logger.info("Closed session %s", session_id)
153
+
154
+ @app.on_event("startup")
155
+ async def _startup():
156
+ async def _cleaner():
157
+ while True:
158
+ _purge_idle()
159
+ await asyncio.sleep(300)
160
+ asyncio.create_task(_cleaner())
 
161
 
162
  # API Endpoints
163
 
164
  @app.get("/health")
165
+ async def health():
166
+ return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
 
167
 
168
  @app.post("/api/browser/launch")
169
+ async def launch(request: BrowserLaunchRequest):
170
  """Launch a new browser instance"""
171
  session_id = str(uuid.uuid4())
172
 
173
  try:
174
  # Launch Playwright browser
175
+ ctx = await browser_pool.new_context(
176
  viewport={'width': request.width, 'height': request.height},
177
  user_agent=request.user_agent
178
  )
179
+ page = await ctx.new_page()
 
 
 
 
 
 
 
 
180
 
181
  # Store session
182
  browser_instances[session_id] = {
183
+ "playwright_context": ctx,
184
+ "playwright_page": page,
185
+ "selenium_driver": driver,
186
+ "created_at": time.time(),
187
+ "config": request.dict()
188
  }
189
+ logger.info("Launched session %s", session_id)
190
+ return {"session_id": session_id, "status": "launched"}
 
 
191
  except Exception as e:
192
  logger.error(f"Error launching browser: {e}")
193
  raise HTTPException(status_code=500, detail=str(e))
194
 
195
  @app.post("/api/browser/navigate")
196
+ async def navigate(req: NavigateRequest):
197
  """Navigate to a URL"""
198
+ if req.session_id not in browser_instances:
199
  raise HTTPException(status_code=404, detail="Session not found")
200
+ sess = browser_instances[req.session_id]
 
 
201
  try:
202
  # Navigate with Playwright
203
+ await sess["playwright_page"].goto(req.url, wait_until=req.wait_until)
204
+ return {"status": "navigated", "url": req.url}
 
 
 
 
 
205
  except Exception as e:
206
+ logger.error(f"Error navigating to {req.url}: {e}")
207
  raise HTTPException(status_code=500, detail=str(e))
208
 
209
  @app.post("/api/browser/screenshot")
210
+ async def screenshot(req: ScreenshotRequest):
211
  """Take a screenshot"""
212
+ if req.session_id not in browser_instances:
213
  raise HTTPException(status_code=404, detail="Session not found")
214
+ sess = browser_instances[req.session_id]
 
215
 
216
  try:
217
+ if req.selector:
218
+ el = sess["playwright_page"].locator(req.selector).first
219
+ png = await el.screenshot()
 
220
  else:
221
  # Full page screenshot with Playwright
222
+ png = await sess["playwright_page"].screenshot(full_page=req.full_page)
 
 
 
223
  # Convert to base64
224
+ b64 = base64.b64encode(png).decode()
225
+ return {"screenshot": b64, "format": "png", "timestamp": datetime.utcnow().isoformat()}
 
 
 
 
 
 
226
  except Exception as e:
227
  logger.error(f"Error taking screenshot: {e}")
228
  raise HTTPException(status_code=500, detail=str(e))
229
 
230
+ @app.delete("/api/browser/close/{session_id}")
231
+ async def close(session_id: str):
232
+ if session_id not in browser_instances:
233
+ raise HTTPException(status_code=404, detail="Session not found")
234
+ await _close_session(session_id)
235
+ return {"status": "closed", "session_id": session_id}
236
+
237
+ # --------------------------------------------------------------------------- #
238
+ # API – Element-level
239
+ # --------------------------------------------------------------------------- #
240
+
241
  @app.post("/api/elements/action")
242
+ async def element_action(req: ElementActionRequest):
243
  """Perform action on an element"""
244
+ if req.session_id not in browser_instances:
245
  raise HTTPException(status_code=404, detail="Session not found")
246
+ sess = browser_instances[req.session_id]
247
+ page = sess["playwright_page"]
248
+ el = page.locator(req.selector).first
249
 
250
  try:
251
+ if req.action == "click":
252
+ await el.click()
253
+ elif req.action == "type":
254
+ await el.fill(req.value or "")
255
+ elif req.action == "scroll":
256
+ await el.scroll_into_view_if_needed()
257
+ elif req.action == "hover":
258
+ await el.hover()
259
+ elif req.action == "textContent":
260
+ text = await el.text_content() or ""
261
+ return {"status": "completed", "action": "textContent", "text": text}
262
  else:
263
+ raise ValueError("unknown action")
264
+ return {"status": "completed", "action": req.action}
 
 
265
  except Exception as e:
266
+ logger.error(f"Error performing action {req.action}: {e}")
267
  raise HTTPException(status_code=500, detail=str(e))
268
 
269
  @app.get("/api/elements/inspect/{session_id}")
270
+ async def inspect(session_id: str):
271
  """Get all interactive elements on the page"""
272
  if session_id not in browser_instances:
273
  raise HTTPException(status_code=404, detail="Session not found")
274
+ page = browser_instances[session_id]["playwright_page"]
275
+ soup = BeautifulSoup(await page.content(), "html.parser")
276
+ selectors = [
277
+ 'a','button','input','select','textarea',
278
+ '[onclick]','[href]','[role="button"]'
279
+ ]
280
+ out = []
281
 
282
  try:
283
+ for sel in selectors:
284
+ for idx, elem in enumerate(soup.select(sel)):
285
+ out.append({
286
+ "tag": elem.name,
287
+ "selector": f"{sel}:nth-of-type({idx+1})",
288
+ "text": elem.get_text(strip=True)[:100],
289
+ "attributes": dict(elem.attrs),
290
+ "type": elem.get("type", "N/A")
291
+ })
292
+ return {"elements": out, "total_count": len(out)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  except Exception as e:
294
  logger.error(f"Error inspecting elements: {e}")
295
  raise HTTPException(status_code=500, detail=str(e))
 
331
  logger.error(f"Error scraping content: {e}")
332
  raise HTTPException(status_code=500, detail=str(e))
333
 
 
 
 
 
 
 
 
 
 
334
  @app.get("/api/sessions")
335
  async def list_sessions():
336
  """List all active browser sessions"""
 
344
 
345
  return {"sessions": sessions, "total_count": len(sessions)}
346
 
 
 
 
 
 
 
 
 
 
 
347
  if __name__ == "__main__":
348
+ uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=False)