"""SnapAPI client implementation.""" from __future__ import annotations import urllib.request import urllib.error import json from dataclasses import dataclass, asdict from typing import Optional class SnapAPIError(Exception): """Raised when the SnapAPI returns an error response.""" def __init__(self, status: int, detail: str): self.status = status self.detail = detail super().__init__(f"SnapAPI error {status}: {detail}") @dataclass class ScreenshotOptions: """Screenshot capture options.""" url: str format: Optional[str] = None width: Optional[int] = None height: Optional[int] = None full_page: Optional[bool] = None quality: Optional[int] = None wait_for_selector: Optional[str] = None device_scale: Optional[float] = None delay: Optional[int] = None wait_until: Optional[str] = None def to_dict(self) -> dict: """Convert to API request body (camelCase keys).""" mapping = { "url": "url", "format": "format", "width": "width", "height": "height", "full_page": "fullPage", "quality": "quality", "wait_for_selector": "waitForSelector", "device_scale": "deviceScale", "delay": "delay", "wait_until": "waitUntil", } return { mapping[k]: v for k, v in asdict(self).items() if v is not None } class SnapAPI: """SnapAPI client. Args: api_key: Your SnapAPI API key. base_url: API base URL (default: https://snapapi.eu). timeout: Request timeout in seconds (default: 30). Example:: from snapapi import SnapAPI snap = SnapAPI("your-api-key") screenshot = snap.capture("https://example.com") with open("screenshot.png", "wb") as f: f.write(screenshot) """ def __init__( self, api_key: str, base_url: str = "https://snapapi.eu", timeout: int = 30, ): if not api_key: raise ValueError("api_key is required") self.api_key = api_key self.base_url = base_url.rstrip("/") self.timeout = timeout def capture( self, url: Optional[str] = None, *, format: Optional[str] = None, width: Optional[int] = None, height: Optional[int] = None, full_page: Optional[bool] = None, quality: Optional[int] = None, wait_for_selector: Optional[str] = None, device_scale: Optional[float] = None, delay: Optional[int] = None, wait_until: Optional[str] = None, options: Optional[ScreenshotOptions] = None, ) -> bytes: """Capture a screenshot. Args: url: URL to capture. format: Output format (png, jpeg, webp). width: Viewport width (320-3840). height: Viewport height (200-2160). full_page: Capture full scrollable page. quality: JPEG/WebP quality (1-100). wait_for_selector: CSS selector to wait for. device_scale: Device pixel ratio (1-3). delay: Extra delay in ms (0-5000). wait_until: Load event (domcontentloaded, load, networkidle0, networkidle2). options: ScreenshotOptions object (alternative to keyword args). Returns: Screenshot image as bytes. Raises: SnapAPIError: When the API returns an error. ValueError: When url is missing. Example:: # Simple png = snap.capture("https://example.com") # With options jpg = snap.capture( "https://example.com", format="jpeg", width=1920, quality=90, ) # Full-page Retina full = snap.capture( "https://example.com", full_page=True, device_scale=2, ) """ if options: body = options.to_dict() else: if not url: raise ValueError("url is required") opts = ScreenshotOptions( url=url, format=format, width=width, height=height, full_page=full_page, quality=quality, wait_for_selector=wait_for_selector, device_scale=device_scale, delay=delay, wait_until=wait_until, ) body = opts.to_dict() data = json.dumps(body).encode() req = urllib.request.Request( f"{self.base_url}/v1/screenshot", data=data, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}", }, method="POST", ) try: with urllib.request.urlopen(req, timeout=self.timeout) as resp: return resp.read() except urllib.error.HTTPError as e: detail = f"HTTP {e.code}" try: err_body = json.loads(e.read()) detail = err_body.get("error", detail) except Exception: pass raise SnapAPIError(e.code, detail) from e def health(self) -> dict: """Check API health status. Returns: Health check response dict. Example:: health = snap.health() print(health["status"]) # "ok" """ req = urllib.request.Request(f"{self.base_url}/health") with urllib.request.urlopen(req, timeout=self.timeout) as resp: return json.loads(resp.read())