All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m1s
- Node.js SDK: TypeScript, ESM+CJS, zero deps (uses native fetch) - Python SDK: zero deps (uses urllib), Python 3.8+ - Both fully documented with examples and type hints - Ready for npm/PyPI publishing
200 lines
5.7 KiB
Python
200 lines
5.7 KiB
Python
"""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())
|