operations / services /web /generic_browser.py
jbbove's picture
🎯 Complete OMIRL web services refactoring with workflow enhancement
36f8fda
"""
Generic Browser Manager Implementation
This module provides a generic, configurable browser manager that can be
used with different website configurations. It separates browser lifecycle
management from site-specific logic.
Features:
- Configurable browser settings (locale, viewport, timeouts)
- Retry logic with exponential backoff
- Rate limiting support
- Context management for multiple sessions
- Resource cleanup and error recovery
Usage:
config = BrowserConfig(locale="it-IT", timezone_id="Europe/Rome")
manager = GenericBrowserManager(config)
context = await manager.get_context("my_session")
page = await context.new_page()
success = await manager.navigate_with_retry(page, "https://example.com")
"""
import asyncio
import time
from typing import Optional, Dict, Any
from playwright.async_api import async_playwright, Browser, BrowserContext, Page, Playwright
from .base import IBrowserManager, BrowserConfig, NavigationError
class GenericBrowserManager(IBrowserManager):
"""Generic browser manager with configurable behavior"""
def __init__(self, config: BrowserConfig):
self.config = config
self.playwright: Optional[Playwright] = None
self.browser: Optional[Browser] = None
self.contexts: Dict[str, BrowserContext] = {}
async def initialize(self):
"""Initialize Playwright and browser if not already done"""
if self.playwright is None:
self.playwright = await async_playwright().start()
if self.browser is None:
self.browser = await self.playwright.chromium.launch(
headless=self.config.headless,
args=self.config.browser_args
)
async def get_context(self, context_id: str = "default") -> BrowserContext:
"""Get or create a browser context with configuration settings"""
await self.initialize()
if context_id not in self.contexts:
self.contexts[context_id] = await self.browser.new_context(
viewport=self.config.viewport,
locale=self.config.locale,
timezone_id=self.config.timezone_id,
user_agent=self.config.user_agent,
extra_http_headers=self.config.extra_headers
)
# Set timeouts
self.contexts[context_id].set_default_timeout(self.config.default_timeout)
self.contexts[context_id].set_default_navigation_timeout(self.config.navigation_timeout)
return self.contexts[context_id]
async def navigate_with_retry(self, page: Page, url: str, max_retries: int = 3) -> bool:
"""Navigate to URL with configurable retry logic"""
for attempt in range(max_retries):
try:
print(f"🌐 Navigating to {url} (attempt {attempt + 1}/{max_retries})")
await page.goto(url, wait_until="networkidle")
# Basic success check - page loaded without error
title = await page.title()
if title and "error" not in title.lower():
print(f"βœ… Successfully loaded page: {title}")
return True
except Exception as e:
print(f"⚠️ Navigation attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
# Exponential backoff
wait_time = 2 ** attempt
print(f"πŸ”„ Retrying in {wait_time}s...")
await asyncio.sleep(wait_time)
print(f"❌ Failed to navigate to {url} after {max_retries} attempts")
raise NavigationError(f"Failed to navigate to {url} after {max_retries} attempts")
async def apply_rate_limiting(self, delay_ms: Optional[int] = None):
"""Apply rate limiting delay"""
delay = delay_ms if delay_ms is not None else self.config.rate_limit_ms
await asyncio.sleep(delay / 1000.0)
async def close_context(self, context_id: str = "default"):
"""Close a specific browser context"""
if context_id in self.contexts:
await self.contexts[context_id].close()
del self.contexts[context_id]
print(f"πŸ”’ Closed browser context: {context_id}")
async def close_all(self):
"""Close all browser resources"""
# Close all contexts
for context_id in list(self.contexts.keys()):
await self.close_context(context_id)
# Close browser
if self.browser:
await self.browser.close()
self.browser = None
print("πŸ”’ Closed browser")
# Stop playwright
if self.playwright:
await self.playwright.stop()
self.playwright = None
print("πŸ”’ Stopped Playwright")
# Factory function for easy instantiation
def create_browser_manager(config: BrowserConfig) -> GenericBrowserManager:
"""Factory function to create a browser manager with configuration"""
return GenericBrowserManager(config)