diff --git a/Dockers/puppeteer-api/app/__pycache__/main.cpython-313.pyc b/Dockers/puppeteer-api/app/__pycache__/main.cpython-313.pyc index e23b098..0a61340 100644 Binary files a/Dockers/puppeteer-api/app/__pycache__/main.cpython-313.pyc and b/Dockers/puppeteer-api/app/__pycache__/main.cpython-313.pyc differ diff --git a/Dockers/puppeteer-api/app/main.py b/Dockers/puppeteer-api/app/main.py index 948b897..7c6e42d 100644 --- a/Dockers/puppeteer-api/app/main.py +++ b/Dockers/puppeteer-api/app/main.py @@ -1,18 +1,83 @@ from fastapi import FastAPI +from fastapi.openapi.utils import get_openapi +from fastapi.security import APIKeyHeader from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from app.database import init_db, cleanup_old_cache_entries from app.config import CLEANUP_CRON, CACHE_EXPIRY_HOURS from app.routes import health, browser, cache from app.services.browser import force_cleanup_old_pages +from pathlib import Path + + +def _read_version() -> str: + version_file = Path(__file__).resolve().parents[1] / "version" + try: + v = version_file.read_text(encoding="utf-8").strip() + return v or "latest" + except Exception: + return "latest" + + +API_KEY_HEADER_NAME = "X-API-Key" +api_key_header = APIKeyHeader(name=API_KEY_HEADER_NAME, auto_error=False) # Create FastAPI app -app = FastAPI() +app = FastAPI( + title="Playwright API Server", + version=_read_version(), + description=( + "FastAPI server that uses Playwright to visit pages, capture screenshots, " + "extract SEO/meta information, and manage a cache.\n\n" + f"Authentication: provide `{API_KEY_HEADER_NAME}` on requests." + ), + docs_url="/docs", + redoc_url="/redoc", + openapi_url="/openapi.json", + swagger_ui_parameters={"persistAuthorization": True}, +) + + +def custom_openapi(): + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + components = openapi_schema.setdefault("components", {}) + security_schemes = components.setdefault("securitySchemes", {}) + security_schemes["ApiKeyAuth"] = { + "type": "apiKey", + "in": "header", + "name": API_KEY_HEADER_NAME, + "description": f"Send your API key in the `{API_KEY_HEADER_NAME}` header.", + } + + # Apply API key auth globally (individual endpoints may still allow anonymous access; + # this just documents the expected auth mechanism). + openapi_schema["security"] = [{"ApiKeyAuth": []}] + + openapi_schema["tags"] = [ + {"name": "Health", "description": "Liveness / basic health checks."}, + {"name": "Browser", "description": "Playwright-powered browsing utilities."}, + {"name": "Cache", "description": "Cache management and statistics."}, + ] + + app.openapi_schema = openapi_schema + return app.openapi_schema + + +app.openapi = custom_openapi # Include routers -app.include_router(health.router) -app.include_router(browser.router) -app.include_router(cache.router) +app.include_router(health.router, tags=["Health"]) +app.include_router(browser.router, tags=["Browser"]) +app.include_router(cache.router, tags=["Cache"]) # Initialize scheduler for periodic cache cleanup scheduler = BackgroundScheduler() diff --git a/Dockers/puppeteer-api/app/routes/browser.py b/Dockers/puppeteer-api/app/routes/browser.py index 7778cad..d293314 100644 --- a/Dockers/puppeteer-api/app/routes/browser.py +++ b/Dockers/puppeteer-api/app/routes/browser.py @@ -14,7 +14,7 @@ from app.services.browser import ( get_resulting_url_service ) -router = APIRouter() +router = APIRouter(tags=["Browser"]) class ClickInteraction(BaseModel): action: Literal["click"] @@ -43,14 +43,21 @@ class Viewport(BaseModel): class InteractionsRequest(BaseModel): - url: str - returnType: Optional[Literal["screenshot", "html"]] = "screenshot" + url: str = Field(..., description="Target URL (will be URL-decoded server-side).") + returnType: Optional[Literal["screenshot", "html"]] = Field( + "screenshot", + description="Return a PNG screenshot or HTML content.", + ) viewport: Optional[Viewport] = None scrollX: Optional[int] = None scrollY: Optional[int] = None interactions: List[Interaction] -@router.get("/") +@router.get( + "/", + summary="Visit URL", + description="Navigate to `url` and return HTML content (cached unless `skipCache=true`).", +) async def visit_url(url: str, skipCache: bool = False, x_api_key: Optional[str] = Header(None)): # Validate API key if not x_api_key or x_api_key != API_KEY: @@ -78,7 +85,16 @@ async def visit_url(url: str, skipCache: bool = False, x_api_key: Optional[str] print(f"Error visiting URL {decoded_url}: {e}") raise HTTPException(status_code=500, detail=str(e)) -@router.get("/screenshot") +@router.get( + "/screenshot", + summary="Capture screenshot", + description="Capture a PNG screenshot for `url`.", + responses={ + 200: {"content": {"image/png": {}}}, + 401: {"description": "Invalid API key"}, + 500: {"description": "Screenshot capture failed"}, + }, +) async def screenshot_url(url: str, fullPage: bool = True, x_api_key: Optional[str] = Header(None)): """Capture a screenshot of a website and return it as PNG""" if not x_api_key or x_api_key != API_KEY: @@ -102,7 +118,26 @@ async def screenshot_url(url: str, fullPage: bool = True, x_api_key: Optional[st print(f"Error taking screenshot for URL {decoded_url}: {e}") raise HTTPException(status_code=500, detail=str(e)) -@router.post("/interactions") +@router.post( + "/interactions", + summary="Run interactions", + description=( + "Run a sequence of interactions (click/type) on a page.\n\n" + "- `returnType=screenshot` returns `image/png`\n" + "- `returnType=html` returns `text/html`" + ), + responses={ + 200: { + "content": { + "image/png": {}, + "text/html": {}, + "application/json": {}, + } + }, + 401: {"description": "Invalid API key"}, + 500: {"description": "Interaction failed"}, + }, +) async def run_interactions(payload: InteractionsRequest, x_api_key: Optional[str] = Header(None)): if not x_api_key or x_api_key != API_KEY: raise HTTPException(status_code=401, detail="Invalid API key") @@ -137,7 +172,11 @@ async def run_interactions(payload: InteractionsRequest, x_api_key: Optional[str print(f"Error running interactions on URL {decoded_url}: {e}") raise HTTPException(status_code=500, detail=str(e)) -@router.get("/seo") +@router.get( + "/seo", + summary="Extract SEO information", + description="Extract SEO information from `url` (cached unless `skipCache=true`).", +) async def extract_seo(url: str, skipCache: bool = False, x_api_key: Optional[str] = Header(None)): """Extract SEO information from a website""" # Validate API key @@ -165,7 +204,11 @@ async def extract_seo(url: str, skipCache: bool = False, x_api_key: Optional[str except Exception as e: raise HTTPException(status_code=500, detail=str(e)) -@router.get("/meta") +@router.get( + "/meta", + summary="Extract meta tags", + description="Extract meta tags / Open Graph / Twitter card data from `url` (cached unless `skipCache=true`).", +) async def extract_meta_tags(url: str, skipCache: bool = False, x_api_key: Optional[str] = Header(None)): """Extract meta tags from a website""" # Validate API key @@ -194,7 +237,11 @@ async def extract_meta_tags(url: str, skipCache: bool = False, x_api_key: Option raise HTTPException(status_code=500, detail=str(e)) -@router.get("/outgoing-calls") +@router.get( + "/outgoing-calls", + summary="Capture outgoing calls", + description="Capture outgoing API calls from `url` (cached unless `skipCache=true`).", +) async def capture_outgoing_calls(url: str, skipCache: bool = False, x_api_key: Optional[str] = Header(None)): """Capture outgoing API calls from a website""" # Validate API key @@ -224,7 +271,11 @@ async def capture_outgoing_calls(url: str, skipCache: bool = False, x_api_key: O raise HTTPException(status_code=500, detail=str(e)) -@router.get("/resulting-url") +@router.get( + "/resulting-url", + summary="Get resulting URL", + description="Get the final URL after navigation/redirects for `url` (cached unless `skipCache=true`).", +) async def get_resulting_url(url: str, skipCache: bool = False, x_api_key: Optional[str] = Header(None)): """Get the resulting URL after navigation (handles redirects)""" # Validate API key diff --git a/Dockers/puppeteer-api/app/routes/cache.py b/Dockers/puppeteer-api/app/routes/cache.py index e61fc77..b5bdb70 100644 --- a/Dockers/puppeteer-api/app/routes/cache.py +++ b/Dockers/puppeteer-api/app/routes/cache.py @@ -3,7 +3,7 @@ from typing import Optional from app.config import API_KEY from app.services.cache import clear_cache, get_cache_stats -router = APIRouter() +router = APIRouter(tags=["Cache"]) @router.get("/cache/clear") async def clear_cache_route(x_api_key: Optional[str] = Header(None)): diff --git a/Dockers/puppeteer-api/app/routes/health.py b/Dockers/puppeteer-api/app/routes/health.py index 6194522..61ca585 100644 --- a/Dockers/puppeteer-api/app/routes/health.py +++ b/Dockers/puppeteer-api/app/routes/health.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -router = APIRouter() +router = APIRouter(tags=["Health"]) @router.head("/") async def health_check():