cleanup
Build and Push Docker Images / build-and-push (push) Successful in 2m32s

This commit is contained in:
2025-07-12 16:26:22 +02:00
parent 5f5941703c
commit 2f724d0c1f
9 changed files with 306 additions and 288 deletions
+29 -2
View File
@@ -52,9 +52,17 @@ This project has been successfully migrated from Puppeteer (pyppeteer) to Playwr
- **System status endpoint**: `/status` - Monitor CPU, memory, file descriptors - **System status endpoint**: `/status` - Monitor CPU, memory, file descriptors
- **Browser pool monitoring**: Track active browsers and their ages - **Browser pool monitoring**: Track active browsers and their ages
- **Emergency cleanup endpoint**: `/emergency-cleanup` - Force cleanup when needed - **Emergency cleanup endpoint**: `/emergency-cleanup` - Force cleanup when needed
- **Timeout-based cleanup**: `/force-cleanup-old` - Force close old browser/page instances
- **Monitoring script**: `monitor.py` - Real-time system monitoring - **Monitoring script**: `monitor.py` - Real-time system monitoring
#### 4. Error Handling #### 4. Timeout Management
- **Configurable timeout**: `BROWSER_INSTANCE_TIMEOUT_MINUTES` (default: 10 minutes)
- **Automatic cleanup**: Old browser and page instances are automatically force closed
- **Page tracking**: All page instances are tracked with creation timestamps
- **Health check integration**: Timeout cleanup runs every 2 minutes as part of health checks
#### 5. Error Handling
- **Timeout handling**: 30s timeout for getting browsers from pool - **Timeout handling**: 30s timeout for getting browsers from pool
- **Emergency recovery**: Auto-cleanup when timeouts occur - **Emergency recovery**: Auto-cleanup when timeouts occur
@@ -70,6 +78,7 @@ API_KEY=your-api-key-here
MAX_BROWSERS=3 # Maximum browser instances (default: 3) MAX_BROWSERS=3 # Maximum browser instances (default: 3)
BROWSER_TTL=1800 # Browser time-to-live in seconds (default: 1800 = 30min) BROWSER_TTL=1800 # Browser time-to-live in seconds (default: 1800 = 30min)
MAX_CONCURRENT_OPERATIONS=5 # Max concurrent operations (default: 5) MAX_CONCURRENT_OPERATIONS=5 # Max concurrent operations (default: 5)
BROWSER_INSTANCE_TIMEOUT_MINUTES=10 # Force close old browser/page instances after N minutes (default: 10)
CACHE_EXPIRY_HOURS=36 # Cache expiry in hours (default: 36) CACHE_EXPIRY_HOURS=36 # Cache expiry in hours (default: 36)
CLEANUP_CRON=0 3 * * * # Cache cleanup schedule (default: daily at 3 AM) CLEANUP_CRON=0 3 * * * # Cache cleanup schedule (default: daily at 3 AM)
RATE_LIMIT_MINUTE=60 # Requests per minute (default: 60) RATE_LIMIT_MINUTE=60 # Requests per minute (default: 60)
@@ -82,12 +91,22 @@ RATE_LIMIT_MINUTE=60 # Requests per minute (default: 60)
- `GET /` - Visit URL and get HTML content - `GET /` - Visit URL and get HTML content
- `GET /seo` - Extract SEO information - `GET /seo` - Extract SEO information
- `GET /meta` - Extract meta tags and Open Graph data - `GET /meta` - Extract meta tags and Open Graph data
- `GET /json` - Fetch JSON content from a website
The `/json` endpoint intelligently extracts JSON data from websites by:
- Directly parsing JSON responses (when Content-Type is application/json)
- Extracting JSON from `<script type="application/json">` tags
- Finding JSON in `data-json` attributes
- Searching for JSON-like structures in page content
- Parsing the entire page content as JSON if possible
### Management Endpoints ### Management Endpoints
- `HEAD /` - Health check - `HEAD /` - Health check
- `GET /status` - System status and browser pool information - `GET /status` - System status and browser pool information
- `POST /emergency-cleanup` - Force cleanup all browsers - `POST /emergency-cleanup` - Force cleanup all browsers
- `POST /force-cleanup-old` - Force cleanup old browser/page instances (based on timeout)
- `GET /cache/stats` - Cache statistics - `GET /cache/stats` - Cache statistics
- `GET /cache/clear` - Clear all cache - `GET /cache/clear` - Clear all cache
@@ -102,8 +121,14 @@ curl -H "X-API-Key: your-api-key" "http://localhost:8000/?url=https://example.co
# Extract SEO data # Extract SEO data
curl -H "X-API-Key: your-api-key" "http://localhost:8000/seo?url=https://example.com" curl -H "X-API-Key: your-api-key" "http://localhost:8000/seo?url=https://example.com"
# Fetch JSON content
curl -H "X-API-Key: your-api-key" "http://localhost:8000/json?url=https://api.example.com/data"
# Get system status # Get system status
curl -H "X-API-Key: your-api-key" "http://localhost:8000/status" curl -H "X-API-Key: your-api-key" "http://localhost:8000/status"
# Force cleanup old browser/page instances
curl -X POST -H "X-API-Key: your-api-key" "http://localhost:8000/force-cleanup-old"
``` ```
### Monitoring ### Monitoring
@@ -157,7 +182,9 @@ This will test:
1. Check system status: `GET /status` 1. Check system status: `GET /status`
2. If browser pool is at capacity, trigger cleanup: `POST /emergency-cleanup` 2. If browser pool is at capacity, trigger cleanup: `POST /emergency-cleanup`
3. Consider reducing `MAX_BROWSERS` or `MAX_CONCURRENT_OPERATIONS` 3. Force cleanup old instances: `POST /force-cleanup-old`
4. Consider reducing `MAX_BROWSERS` or `MAX_CONCURRENT_OPERATIONS`
5. Adjust `BROWSER_INSTANCE_TIMEOUT_MINUTES` to a lower value (e.g., 5 minutes)
### Resource Errors (Errno 11) ### Resource Errors (Errno 11)
+3
View File
@@ -11,6 +11,9 @@ CACHE_EXPIRY_HOURS = int(os.getenv('CACHE_EXPIRY_HOURS', '36'))
# Get cleanup cron schedule from environment variable (default: every day at 3 AM) # Get cleanup cron schedule from environment variable (default: every day at 3 AM)
CLEANUP_CRON = os.getenv('CLEANUP_CRON', '0 3 * * *') CLEANUP_CRON = os.getenv('CLEANUP_CRON', '0 3 * * *')
# Get browser instance timeout from environment variable (default: 10 minutes)
BROWSER_INSTANCE_TIMEOUT_MINUTES = int(os.getenv('BROWSER_INSTANCE_TIMEOUT_MINUTES', '10'))
# Define custom user agent # Define custom user agent
CUSTOM_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36' CUSTOM_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'
+31 -1
View File
@@ -8,7 +8,8 @@ from app.services.browser import (
extract_seo_service, extract_seo_service,
extract_meta_tags_service, extract_meta_tags_service,
detect_pagination_service, detect_pagination_service,
capture_outgoing_calls_service capture_outgoing_calls_service,
fetch_json_service
) )
router = APIRouter() router = APIRouter()
@@ -153,3 +154,32 @@ async def capture_outgoing_calls(url: str, skipCache: bool = False, x_api_key: O
except Exception as e: except Exception as e:
print(f"Error capturing outgoing calls for URL {decoded_url}: {e}") print(f"Error capturing outgoing calls for URL {decoded_url}: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.get("/json")
async def fetch_json(url: str, skipCache: bool = False, x_api_key: Optional[str] = Header(None)):
"""Fetch JSON content from a website"""
# Validate API key
if not x_api_key or x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid API key")
# Decode URL if it's encoded
decoded_url = unquote(url)
# Check cache first (unless skipCache is True)
if not skipCache:
cached_result = get_cached_data(decoded_url, "json")
if cached_result:
return cached_result
try:
# Call the service function
result = await fetch_json_service(decoded_url)
# Save to cache
if(result["status"] == "success"):
save_to_cache(decoded_url, "json", result)
return result
except Exception as e:
print(f"Error fetching JSON for URL {decoded_url}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@@ -894,3 +894,107 @@ async def capture_outgoing_calls_service(decoded_url):
# Perform the operation # Perform the operation
return await safe_browser_operation(decoded_url, outgoing_calls_operation) return await safe_browser_operation(decoded_url, outgoing_calls_operation)
async def fetch_json_service(decoded_url):
"""Service function to fetch JSON content from a website"""
print(f"Fetching JSON from: {decoded_url}")
# Define the operation to perform with the browser
async def json_operation(page):
try:
# Navigate to the URL
response = await page.goto(decoded_url, wait_until='networkidle', timeout=30000)
if not response:
raise Exception("No response received from the URL")
# Check if the response is JSON
content_type = response.headers.get('content-type', '').lower()
if 'application/json' not in content_type and 'text/json' not in content_type:
# If not JSON, try to find JSON content in the page
json_content = await page.evaluate('''() => {
// Look for JSON in script tags
const scripts = document.querySelectorAll('script[type="application/json"], script[type="application/ld+json"]');
if (scripts.length > 0) {
return Array.from(scripts).map(script => {
try {
return JSON.parse(script.textContent);
} catch (e) {
return null;
}
}).filter(json => json !== null);
}
// Look for JSON in data attributes
const elementsWithData = document.querySelectorAll('[data-json]');
if (elementsWithData.length > 0) {
return Array.from(elementsWithData).map(el => {
try {
return JSON.parse(el.getAttribute('data-json'));
} catch (e) {
return null;
}
}).filter(json => json !== null);
}
// Look for JSON in the page content (try to find JSON-like structures)
const bodyText = document.body.innerText;
const jsonMatches = bodyText.match(/\\{[^{}]*\\}/g);
if (jsonMatches) {
const validJsons = [];
for (const match of jsonMatches) {
try {
const parsed = JSON.parse(match);
validJsons.push(parsed);
} catch (e) {
// Skip invalid JSON
}
}
if (validJsons.length > 0) {
return validJsons;
}
}
return null;
}''')
if json_content:
return {
"status": "success",
"url": decoded_url,
"content_type": "json_extracted",
"json_data": json_content
}
else:
# Try to get the page content and check if it's JSON
content = await page.content()
try:
import json
json_data = json.loads(content)
return {
"status": "success",
"url": decoded_url,
"content_type": "json_direct",
"json_data": json_data
}
except json.JSONDecodeError:
raise Exception("No JSON content found on the page")
else:
# Response is already JSON
try:
json_data = await response.json()
return {
"status": "success",
"url": decoded_url,
"content_type": "json_response",
"json_data": json_data
}
except Exception as e:
raise Exception(f"Failed to parse JSON response: {str(e)}")
except Exception as e:
print(f"Error during JSON fetching: {e}")
return {"status": "error", "url": decoded_url, "error": str(e)}
# Perform the operation
return await safe_browser_operation(decoded_url, json_operation)
@@ -1,6 +1,11 @@
from playwright.async_api import async_playwright from playwright.async_api import async_playwright
from app.config import CUSTOM_USER_AGENT from app.config import CUSTOM_USER_AGENT, BROWSER_INSTANCE_TIMEOUT_MINUTES
import asyncio import asyncio
import time
# Global tracking for browser instances created by this module
page_creation_times = {}
active_pages = set()
async def wait_for_network_idle(page): async def wait_for_network_idle(page):
"""Wait until no network requests are in flight""" """Wait until no network requests are in flight"""
@@ -32,6 +37,10 @@ async def safe_browser_operation(url, operation_func):
page = await context.new_page() page = await context.new_page()
page.set_default_timeout(30000) page.set_default_timeout(30000)
# Track page creation time for force cleanup
page_creation_times[page] = time.time()
active_pages.add(page)
# Call the operation function that uses the page # Call the operation function that uses the page
result = await operation_func(page) result = await operation_func(page)
@@ -46,9 +55,14 @@ async def safe_browser_operation(url, operation_func):
"url": url "url": url
} }
finally: finally:
# Ensure page is closed properly # Cleanup page tracking
if page: if page:
try: try:
# Remove from tracking
if page in active_pages:
active_pages.remove(page)
if page in page_creation_times:
del page_creation_times[page]
await page.close() await page.close()
except Exception as e: except Exception as e:
print(f"Error closing page: {e}") print(f"Error closing page: {e}")
@@ -73,3 +87,32 @@ async def safe_browser_operation(url, operation_func):
await playwright.stop() await playwright.stop()
except Exception as e: except Exception as e:
print(f"Error closing playwright: {e}") print(f"Error closing playwright: {e}")
async def force_cleanup_old_pages():
"""Force cleanup old page instances created by this module"""
print(f"Checking for old page instances in browser_utils (timeout: {BROWSER_INSTANCE_TIMEOUT_MINUTES} minutes)...")
current_time = time.time()
timeout_seconds = BROWSER_INSTANCE_TIMEOUT_MINUTES * 60
cleaned_pages = 0
# Clean up old pages
pages_to_cleanup = []
for page in list(active_pages):
creation_time = page_creation_times.get(page, 0)
if current_time - creation_time > timeout_seconds:
pages_to_cleanup.append(page)
print(f"Marking page for cleanup (age: {(current_time - creation_time)/60:.1f} minutes)")
for page in pages_to_cleanup:
try:
if page in active_pages:
active_pages.remove(page)
if page in page_creation_times:
del page_creation_times[page]
await page.close()
cleaned_pages += 1
except Exception as e:
print(f"Error cleaning up old page: {e}")
print(f"Force cleanup completed: {cleaned_pages} pages cleaned")
+93 -2
View File
@@ -14,10 +14,12 @@ from urllib.parse import unquote
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from fastapi.middleware.base import BaseHTTPMiddleware from fastapi.middleware.base import BaseHTTPMiddleware
from app.config import BROWSER_INSTANCE_TIMEOUT_MINUTES
# Add imports for browser pool # Add imports for browser pool
from asyncio import Queue, Lock, Semaphore from asyncio import Queue, Lock, Semaphore
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from app.utils.browser_utils import force_cleanup_old_pages
app = FastAPI() app = FastAPI()
@@ -44,7 +46,9 @@ MAX_CONCURRENT_OPERATIONS = int(os.getenv('MAX_CONCURRENT_OPERATIONS', '5')) #
browser_pool = Queue(maxsize=MAX_BROWSERS) # Add maxsize to prevent unbounded growth browser_pool = Queue(maxsize=MAX_BROWSERS) # Add maxsize to prevent unbounded growth
browser_lock = Lock() browser_lock = Lock()
browser_creation_times = {} browser_creation_times = {}
page_creation_times = {} # Track page creation times for force cleanup
active_browsers = set() # Track active browsers active_browsers = set() # Track active browsers
active_pages = set() # Track active pages for force cleanup
operation_semaphore = Semaphore(MAX_CONCURRENT_OPERATIONS) # Limit concurrent operations operation_semaphore = Semaphore(MAX_CONCURRENT_OPERATIONS) # Limit concurrent operations
playwright_instance = None # Global playwright instance playwright_instance = None # Global playwright instance
@@ -152,10 +156,14 @@ async def cleanup_browser(browser):
if browser in browser_creation_times: if browser in browser_creation_times:
del browser_creation_times[browser] del browser_creation_times[browser]
# Close all pages first # Close all pages first and clean up page tracking
pages = browser.contexts[0].pages if browser.contexts else [] pages = browser.contexts[0].pages if browser.contexts else []
for page in pages: for page in pages:
try: try:
if page in active_pages:
active_pages.remove(page)
if page in page_creation_times:
del page_creation_times[page]
await page.close() await page.close()
except Exception as e: except Exception as e:
print(f"Error closing page: {e}") print(f"Error closing page: {e}")
@@ -173,6 +181,9 @@ async def check_browser_health():
# Sleep for 2 minutes between checks (reduced from 5 minutes) # Sleep for 2 minutes between checks (reduced from 5 minutes)
await asyncio.sleep(120) await asyncio.sleep(120)
# First, force cleanup old instances
await force_cleanup_old_instances()
async with browser_lock: async with browser_lock:
# Get all browsers from the pool # Get all browsers from the pool
browsers = [] browsers = []
@@ -223,6 +234,58 @@ async def check_browser_health():
print(f"Error in browser health check: {e}") print(f"Error in browser health check: {e}")
await asyncio.sleep(60) # Wait before retrying await asyncio.sleep(60) # Wait before retrying
async def force_cleanup_old_instances():
"""Force cleanup old browser and page instances based on timeout"""
print(f"Checking for old browser/page instances (timeout: {BROWSER_INSTANCE_TIMEOUT_MINUTES} minutes)...")
current_time = time.time()
timeout_seconds = BROWSER_INSTANCE_TIMEOUT_MINUTES * 60
cleaned_browsers = 0
cleaned_pages = 0
async with browser_lock:
# Clean up old browsers
browsers_to_cleanup = []
for browser in list(active_browsers):
creation_time = browser_creation_times.get(browser, 0)
if current_time - creation_time > timeout_seconds:
browsers_to_cleanup.append(browser)
print(f"Marking browser for cleanup (age: {(current_time - creation_time)/60:.1f} minutes)")
for browser in browsers_to_cleanup:
try:
await cleanup_browser(browser)
cleaned_browsers += 1
except Exception as e:
print(f"Error cleaning up old browser: {e}")
# Clean up old pages (this is a fallback for pages that might not be properly tracked)
for browser in list(active_browsers):
try:
if browser.contexts:
for context in browser.contexts:
for page in context.pages:
if page in page_creation_times:
creation_time = page_creation_times[page]
if current_time - creation_time > timeout_seconds:
try:
if page in active_pages:
active_pages.remove(page)
if page in page_creation_times:
del page_creation_times[page]
await page.close()
cleaned_pages += 1
print(f"Force closed old page (age: {(current_time - creation_time)/60:.1f} minutes)")
except Exception as e:
print(f"Error closing old page: {e}")
except Exception as e:
print(f"Error checking pages in browser: {e}")
# Also cleanup pages from browser_utils module
await force_cleanup_old_pages()
print(f"Force cleanup completed: {cleaned_browsers} browsers, {cleaned_pages} pages cleaned")
async def force_cleanup_all_browsers(): async def force_cleanup_all_browsers():
"""Force cleanup all browsers - useful for emergency situations""" """Force cleanup all browsers - useful for emergency situations"""
print("Force cleaning up all browsers...") print("Force cleaning up all browsers...")
@@ -439,6 +502,10 @@ async def safe_browser_operation(url, operation_func):
page = await context.new_page() page = await context.new_page()
# Track page creation time for force cleanup
page_creation_times[page] = time.time()
active_pages.add(page)
# Set up request interception for better performance # Set up request interception for better performance
await page.route("**/*", lambda route: route.abort() await page.route("**/*", lambda route: route.abort()
if route.request.resource_type in ['image', 'stylesheet', 'font', 'media'] if route.request.resource_type in ['image', 'stylesheet', 'font', 'media']
@@ -455,6 +522,11 @@ async def safe_browser_operation(url, operation_func):
# Cleanup # Cleanup
if page: if page:
try: try:
# Remove from tracking
if page in active_pages:
active_pages.remove(page)
if page in page_creation_times:
del page_creation_times[page]
await page.close() await page.close()
except: except:
pass pass
@@ -757,6 +829,7 @@ async def system_status(x_api_key: Optional[str] = Header(None)):
# Get browser pool information # Get browser pool information
pool_size = browser_pool.qsize() pool_size = browser_pool.qsize()
active_browser_count = len(active_browsers) active_browser_count = len(active_browsers)
active_page_count = len(active_pages)
# Get cache statistics # Get cache statistics
conn = sqlite3.connect('/db/cache.db') conn = sqlite3.connect('/db/cache.db')
@@ -778,7 +851,9 @@ async def system_status(x_api_key: Optional[str] = Header(None)):
"pool_size": pool_size, "pool_size": pool_size,
"max_browsers": MAX_BROWSERS, "max_browsers": MAX_BROWSERS,
"active_browsers": active_browser_count, "active_browsers": active_browser_count,
"browser_ttl_seconds": BROWSER_TTL "active_pages": active_page_count,
"browser_ttl_seconds": BROWSER_TTL,
"instance_timeout_minutes": BROWSER_INSTANCE_TIMEOUT_MINUTES
}, },
"cache": { "cache": {
"total_entries": cache_count, "total_entries": cache_count,
@@ -805,6 +880,22 @@ async def emergency_cleanup(x_api_key: Optional[str] = Header(None)):
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@app.post("/force-cleanup-old")
async def force_cleanup_old(x_api_key: Optional[str] = Header(None)):
"""Force cleanup old browser and page instances based on timeout"""
# Validate API key
if not x_api_key or x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid API key")
try:
await force_cleanup_old_instances()
return {
"status": "success",
"message": f"Force cleanup of old instances completed (timeout: {BROWSER_INSTANCE_TIMEOUT_MINUTES} minutes)"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@asynccontextmanager @asynccontextmanager
async def get_browser(): async def get_browser():
"""Context manager for getting a browser from the pool""" """Context manager for getting a browser from the pool"""
-59
View File
@@ -1,59 +0,0 @@
#!/usr/bin/env python3
"""
Test script for the pagination route to verify error handling and browser cleanup.
"""
import asyncio
import aiohttp
import json
import sys
async def test_pagination(url, api_key):
"""Test the pagination endpoint with a given URL"""
async with aiohttp.ClientSession() as session:
headers = {"X-API-Key": api_key}
# Test URL with pagination
test_url = f"http://localhost:8000/pagination?url={url}"
print(f"Testing pagination for: {url}")
try:
async with session.get(test_url, headers=headers) as response:
if response.status == 200:
result = await response.json()
print(f"✅ Success: {json.dumps(result, indent=2)}")
return True
else:
error_text = await response.text()
print(f"❌ Error {response.status}: {error_text}")
return False
except Exception as e:
print(f"❌ Exception: {e}")
return False
async def main():
"""Main test function"""
# Test URLs - some with pagination, some without
test_urls = [
"https://example.com", # No pagination
"https://httpbin.org/get", # No pagination
"https://news.ycombinator.com", # Has pagination
]
api_key = "test-key" # Replace with your actual API key
print("Testing pagination route...")
print("=" * 50)
for url in test_urls:
success = await test_pagination(url, api_key)
print("-" * 30)
# Small delay between tests
await asyncio.sleep(1)
print("Test completed!")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,101 +0,0 @@
#!/usr/bin/env python3
import asyncio
import json
# Mock the pagination detection logic to test the URL template generation
async def test_url_template_generation():
"""Test the URL template generation for the specific case"""
# Test case: /vacatures -> /vacatures/page/2
original_url = "https://www.werkenbijabnamro.nl/vacatures"
current_url = "https://www.werkenbijabnamro.nl/vacatures/page/2"
# Simulate the JavaScript logic for path difference detection
def detect_path_diff(original_url, current_url):
from urllib.parse import urlparse
original_parsed = urlparse(original_url)
current_parsed = urlparse(current_url)
original_path = original_parsed.path
current_path = current_parsed.path
if original_path != current_path:
original_segments = [s for s in original_path.split('/') if s]
current_segments = [s for s in current_path.split('/') if s]
# Case 2: Current path has more segments - check for added pagination segments
if len(current_segments) > len(original_segments):
# Look for patterns like /page/NUMBER or /p/NUMBER at the end
import re
page_pattern = re.compile(r'^(page|p)/(\d+)$', re.IGNORECASE)
# Check the last two segments of the current path
if len(current_segments) >= 2:
last_two_segments = '/'.join(current_segments[-2:])
match = page_pattern.match(last_two_segments)
if match:
return {
'type': 'append',
'pageSegment': match.group(1), # 'page' or 'p'
'pageNumber': match.group(2), # the actual number
'originalSegments': original_segments,
'currentSegments': current_segments
}
# If no pattern match, check if the last segment is numeric
if len(current_segments) > 0:
last_segment = current_segments[-1]
if last_segment.isdigit():
return {
'type': 'append',
'pageSegment': None,
'pageNumber': last_segment,
'originalSegments': original_segments,
'currentSegments': current_segments
}
return None
# Test the path difference detection
path_diff = detect_path_diff(original_url, current_url)
print("Path difference detection:")
print(json.dumps(path_diff, indent=2))
# Test URL template generation
def create_url_template(original_url, path_diff):
from urllib.parse import urlparse, urlunparse
if path_diff and path_diff.get('type') == 'append':
url_obj = urlparse(original_url)
new_path = url_obj.path
# Remove trailing slash if present
if new_path.endswith('/'):
new_path = new_path[:-1]
# Append the pagination segment
page_segment = path_diff.get('pageSegment')
if page_segment:
new_path += f'/{page_segment}/{{PAGE_NUMBER}}'
else:
new_path += '/{PAGE_NUMBER}'
url_obj = url_obj._replace(pathname=new_path)
return urlunparse(url_obj)
return None
url_template = create_url_template(original_url, path_diff)
print(f"\nGenerated URL template: {url_template}")
# Test with different page numbers
if url_template:
for page_num in [1, 2, 3, 10]:
test_url = url_template.replace('{PAGE_NUMBER}', str(page_num))
print(f"Page {page_num}: {test_url}")
if __name__ == "__main__":
asyncio.run(test_url_template_generation())
@@ -1,120 +0,0 @@
#!/usr/bin/env python3
"""
Test script to verify Playwright migration works correctly
"""
import asyncio
import sys
import os
# Add the current directory to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from playwright.async_api import async_playwright
async def test_playwright_installation():
"""Test that Playwright is properly installed and can launch a browser"""
print("Testing Playwright installation...")
try:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
page = await context.new_page()
# Test basic navigation
response = await page.goto('https://example.com', wait_until='networkidle', timeout=10000)
title = await page.title()
print(f"✅ Successfully navigated to example.com")
print(f" Title: {title}")
print(f" Status: {response.status if response else 'No response'}")
await context.close()
await browser.close()
return True
except Exception as e:
print(f"❌ Playwright test failed: {e}")
return False
async def test_browser_utils():
"""Test the updated browser_utils module"""
print("\nTesting browser_utils module...")
try:
from app.utils.browser_utils import safe_browser_operation
async def test_operation(page):
await page.goto('https://example.com', wait_until='networkidle', timeout=10000)
title = await page.title()
return {"status": "success", "title": title}
result = await safe_browser_operation('https://example.com', test_operation)
if result.get('status') == 'success':
print(f"✅ browser_utils test passed")
print(f" Title: {result.get('title')}")
return True
else:
print(f"❌ browser_utils test failed: {result}")
return False
except Exception as e:
print(f"❌ browser_utils test failed: {e}")
return False
async def test_browser_service():
"""Test the updated browser service"""
print("\nTesting browser service...")
try:
from app.services.browser import visit_url_service
result = await visit_url_service('https://example.com')
if result.get('status') == 'success':
print(f"✅ browser service test passed")
print(f" Content length: {len(result.get('content', ''))}")
return True
else:
print(f"❌ browser service test failed: {result}")
return False
except Exception as e:
print(f"❌ browser service test failed: {e}")
return False
async def main():
"""Run all tests"""
print("🧪 Running Playwright migration tests...\n")
tests = [
test_playwright_installation,
test_browser_utils,
test_browser_service,
]
results = []
for test in tests:
try:
result = await test()
results.append(result)
except Exception as e:
print(f"❌ Test {test.__name__} failed with exception: {e}")
results.append(False)
print(f"\n📊 Test Results:")
print(f" Passed: {sum(results)}/{len(results)}")
if all(results):
print("🎉 All tests passed! Playwright migration is working correctly.")
return 0
else:
print("❌ Some tests failed. Please check the errors above.")
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)