Entelechy / browser /client.py
qa296
refactor: standardize type hints and improve null safety across codebase
6d49dc7
"""Playwright browser client with session persistence."""
from __future__ import annotations
import base64
from pathlib import Path
from typing import Any
from loguru import logger
from browser.session_manager import SessionManager
class BrowserClient:
"""Playwright browser automation with persistent sessions."""
def __init__(self, profile_path: Path, headless: bool = True):
self.session_manager = SessionManager(profile_path)
self.headless = headless
self.playwright = None
self.browser = None
self.context = None
self.page = None
self._started = False
def _require_page(self) -> Any:
if self.page is None:
raise RuntimeError("Browser page is not initialized")
return self.page
def _require_context(self) -> Any:
if self.context is None:
raise RuntimeError("Browser context is not initialized")
return self.context
async def start(self):
"""Launch the browser and restore session state."""
if self._started:
return
from playwright.async_api import async_playwright
self.playwright = await async_playwright().start()
self.browser = await self.playwright.chromium.launch(headless=self.headless)
# Create context with saved state if available
context_opts = {}
if self.session_manager.has_saved_state():
context_opts["storage_state"] = self.session_manager.get_state_path()
logger.info("Restoring browser session state")
self.context = await self.browser.new_context(**context_opts)
self.page = await self.context.new_page()
self._started = True
logger.info("Browser started")
async def stop(self):
"""Save state and close the browser."""
if not self._started:
return
try:
if self.context:
await self.session_manager.save_state(self.context)
except Exception as e:
logger.warning(f"Error saving state on stop: {e}")
try:
if self.browser:
await self.browser.close()
if self.playwright:
await self.playwright.stop()
except Exception as e:
logger.warning(f"Error closing browser: {e}")
self._started = False
logger.info("Browser stopped")
async def ensure_started(self):
"""Ensure the browser is running."""
if not self._started:
await self.start()
async def navigate(self, url: str) -> str:
"""Navigate to a URL.
Returns:
The page title after navigation.
"""
await self.ensure_started()
try:
page = self._require_page()
context = self._require_context()
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
title = await page.title()
await self.session_manager.save_state(context)
return f"Navigated to: {url}\nTitle: {title}"
except Exception as e:
return f"Navigation error: {e}"
async def click(self, selector: str) -> str:
"""Click an element."""
await self.ensure_started()
try:
page = self._require_page()
await page.click(selector, timeout=10000)
await page.wait_for_load_state("domcontentloaded")
return f"Clicked: {selector}"
except Exception as e:
return f"Click error: {e}"
async def type_text(self, selector: str, text: str) -> str:
"""Type text into an element."""
await self.ensure_started()
try:
page = self._require_page()
await page.fill(selector, text, timeout=10000)
return f"Typed text into: {selector}"
except Exception as e:
return f"Type error: {e}"
async def screenshot(self, full_page: bool = True) -> str:
"""Take a screenshot and return as base64.
Returns:
Base64 encoded PNG image string.
"""
await self.ensure_started()
try:
page = self._require_page()
raw = await page.screenshot(full_page=full_page)
encoded = base64.b64encode(raw).decode("utf-8")
return f"Screenshot taken ({len(raw)} bytes). Base64: {encoded[:100]}..."
except Exception as e:
return f"Screenshot error: {e}"
async def extract_content(self) -> str:
"""Extract text content from the current page."""
await self.ensure_started()
try:
page = self._require_page()
title = await page.title()
url = page.url
text = await page.inner_text("body")
# Truncate very long pages
if len(text) > 50000:
text = text[:50000] + "\n\n... (truncated)"
return f"URL: {url}\nTitle: {title}\n\n{text}"
except Exception as e:
return f"Extract error: {e}"
async def execute_action(self, action: str, **kwargs) -> str:
"""Execute a browser action by name."""
if action == "navigate":
return await self.navigate(kwargs.get("url", ""))
elif action == "click":
return await self.click(kwargs.get("selector", ""))
elif action == "type":
return await self.type_text(
kwargs.get("selector", ""), kwargs.get("text", "")
)
elif action == "screenshot":
return await self.screenshot()
elif action == "extract":
return await self.extract_content()
else:
return f"Unknown browser action: {action}"