From 66ecc471cfd9d3e0ba3d87a86c64793fa98df8f6 Mon Sep 17 00:00:00 2001 From: OpenClawd Date: Mon, 23 Feb 2026 14:02:15 +0000 Subject: [PATCH] feat: add Node.js and Python SDKs - 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 --- sdk/node/README.md | 126 ++++++++++++++++++ sdk/node/package.json | 34 +++++ sdk/node/src/index.ts | 184 ++++++++++++++++++++++++++ sdk/node/tsconfig.json | 14 ++ sdk/python/README.md | 120 +++++++++++++++++ sdk/python/pyproject.toml | 25 ++++ sdk/python/src/snapapi/__init__.py | 6 + sdk/python/src/snapapi/client.py | 200 +++++++++++++++++++++++++++++ 8 files changed, 709 insertions(+) create mode 100644 sdk/node/README.md create mode 100644 sdk/node/package.json create mode 100644 sdk/node/src/index.ts create mode 100644 sdk/node/tsconfig.json create mode 100644 sdk/python/README.md create mode 100644 sdk/python/pyproject.toml create mode 100644 sdk/python/src/snapapi/__init__.py create mode 100644 sdk/python/src/snapapi/client.py diff --git a/sdk/node/README.md b/sdk/node/README.md new file mode 100644 index 0000000..eb7ee63 --- /dev/null +++ b/sdk/node/README.md @@ -0,0 +1,126 @@ +# SnapAPI Node.js SDK + +Official Node.js client for [SnapAPI](https://snapapi.eu) — the EU-hosted screenshot API. + +## Installation + +```bash +npm install snapapi +``` + +## Quick Start + +```typescript +import { SnapAPI } from 'snapapi'; +import fs from 'fs'; + +const snap = new SnapAPI('your-api-key'); + +// Simple screenshot +const png = await snap.capture('https://example.com'); +fs.writeFileSync('screenshot.png', png); +``` + +## Usage + +### Basic Screenshot + +```typescript +const screenshot = await snap.capture('https://example.com'); +``` + +### With Options + +```typescript +const screenshot = await snap.capture('https://example.com', { + format: 'jpeg', + width: 1920, + height: 1080, + quality: 90, +}); +``` + +### Full-Page Capture + +```typescript +const screenshot = await snap.capture({ + url: 'https://example.com/blog', + fullPage: true, + format: 'png', + deviceScale: 2, // Retina +}); +``` + +### Mobile Viewport + +```typescript +const screenshot = await snap.capture({ + url: 'https://example.com', + width: 375, + height: 812, + deviceScale: 2, +}); +``` + +### Wait for Dynamic Content + +```typescript +const screenshot = await snap.capture({ + url: 'https://example.com/dashboard', + waitForSelector: '#chart-loaded', + waitUntil: 'networkidle2', +}); +``` + +## API Reference + +### `new SnapAPI(apiKey, config?)` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `apiKey` | `string` | Your SnapAPI API key (required) | +| `config.baseUrl` | `string` | API base URL (default: `https://snapapi.eu`) | +| `config.timeout` | `number` | Request timeout in ms (default: `30000`) | + +### `snap.capture(url, options?)` / `snap.capture(options)` + +Returns a `Promise` containing the screenshot image. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `url` | `string` | — | URL to capture (required) | +| `format` | `'png' \| 'jpeg' \| 'webp'` | `'png'` | Output format | +| `width` | `number` | `1280` | Viewport width (320–3840) | +| `height` | `number` | `800` | Viewport height (200–2160) | +| `fullPage` | `boolean` | `false` | Capture full scrollable page | +| `quality` | `number` | `80` | JPEG/WebP quality (1–100) | +| `waitForSelector` | `string` | — | CSS selector to wait for | +| `deviceScale` | `number` | `1` | Device pixel ratio (1–3) | +| `delay` | `number` | `0` | Extra delay in ms (0–5000) | +| `waitUntil` | `string` | `'domcontentloaded'` | Load event to wait for | + +### `snap.health()` + +Returns API health status. + +### Error Handling + +```typescript +import { SnapAPI, SnapAPIError } from 'snapapi'; + +try { + const screenshot = await snap.capture('https://example.com'); +} catch (err) { + if (err instanceof SnapAPIError) { + console.error(`API error ${err.status}: ${err.detail}`); + } +} +``` + +## EU-Hosted & GDPR Compliant + +SnapAPI runs entirely on EU infrastructure (Germany). Your data never leaves the EU. [Learn more](https://snapapi.eu). + +## License + +MIT — [Cloonar Technologies GmbH](https://snapapi.eu) diff --git a/sdk/node/package.json b/sdk/node/package.json new file mode 100644 index 0000000..234cbb5 --- /dev/null +++ b/sdk/node/package.json @@ -0,0 +1,34 @@ +{ + "name": "snapapi", + "version": "1.0.0", + "description": "Official Node.js SDK for SnapAPI — EU-hosted screenshot API", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": ["dist"], + "scripts": { + "build": "tsc && tsc --module commonjs --outDir dist/cjs && mv dist/cjs/index.js dist/index.cjs && rm -rf dist/cjs", + "prepublishOnly": "npm run build" + }, + "keywords": ["screenshot", "api", "webpage", "capture", "puppeteer", "headless", "eu", "gdpr"], + "author": "Cloonar Technologies GmbH", + "license": "MIT", + "homepage": "https://snapapi.eu", + "repository": { + "type": "git", + "url": "https://git.cloonar.com/openclawd/SnapAPI" + }, + "engines": { + "node": ">=18" + }, + "devDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/sdk/node/src/index.ts b/sdk/node/src/index.ts new file mode 100644 index 0000000..662c75e --- /dev/null +++ b/sdk/node/src/index.ts @@ -0,0 +1,184 @@ +/** + * SnapAPI Node.js SDK + * Official client for https://snapapi.eu — EU-hosted screenshot API + * + * @example + * ```typescript + * import { SnapAPI } from 'snapapi'; + * + * const snap = new SnapAPI('your-api-key'); + * const screenshot = await snap.capture('https://example.com'); + * fs.writeFileSync('screenshot.png', screenshot); + * ``` + */ + +export interface ScreenshotOptions { + /** URL to capture (required) */ + url: string; + /** Output format: png, jpeg, or webp (default: png) */ + format?: "png" | "jpeg" | "webp"; + /** Viewport width in pixels, 320-3840 (default: 1280) */ + width?: number; + /** Viewport height in pixels, 200-2160 (default: 800) */ + height?: number; + /** Capture full scrollable page (default: false) */ + fullPage?: boolean; + /** JPEG/WebP quality 1-100 (default: 80, ignored for PNG) */ + quality?: number; + /** CSS selector to wait for before capturing */ + waitForSelector?: string; + /** Device scale factor 1-3 (default: 1, use 2 for Retina) */ + deviceScale?: number; + /** Extra delay in ms after page load, 0-5000 (default: 0) */ + delay?: number; + /** Page load event to wait for (default: domcontentloaded) */ + waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2"; +} + +export interface SnapAPIConfig { + /** API base URL (default: https://snapapi.eu) */ + baseUrl?: string; + /** Request timeout in ms (default: 30000) */ + timeout?: number; +} + +export class SnapAPIError extends Error { + /** HTTP status code */ + status: number; + /** Error message from the API */ + detail: string; + + constructor(status: number, detail: string) { + super(`SnapAPI error ${status}: ${detail}`); + this.name = "SnapAPIError"; + this.status = status; + this.detail = detail; + } +} + +export class SnapAPI { + private apiKey: string; + private baseUrl: string; + private timeout: number; + + /** + * Create a new SnapAPI client. + * + * @param apiKey - Your SnapAPI API key + * @param config - Optional configuration + * + * @example + * ```typescript + * const snap = new SnapAPI('sk_live_...'); + * ``` + */ + constructor(apiKey: string, config?: SnapAPIConfig) { + if (!apiKey) throw new Error("SnapAPI: apiKey is required"); + this.apiKey = apiKey; + this.baseUrl = (config?.baseUrl ?? "https://snapapi.eu").replace(/\/$/, ""); + this.timeout = config?.timeout ?? 30000; + } + + /** + * Capture a screenshot of a URL. + * + * Returns the screenshot as a Buffer (Node.js) or Uint8Array. + * + * @param urlOrOptions - URL string or full ScreenshotOptions object + * @param options - Additional options when first arg is a URL string + * @returns Screenshot image as Buffer + * + * @example + * ```typescript + * // Simple usage + * const png = await snap.capture('https://example.com'); + * + * // With options + * const jpg = await snap.capture('https://example.com', { + * format: 'jpeg', + * width: 1920, + * height: 1080, + * quality: 90 + * }); + * + * // Full-page capture + * const full = await snap.capture({ + * url: 'https://example.com', + * fullPage: true, + * format: 'png', + * deviceScale: 2 + * }); + * ``` + * + * @throws {SnapAPIError} When the API returns an error response + */ + async capture( + urlOrOptions: string | ScreenshotOptions, + options?: Omit + ): Promise { + const body: ScreenshotOptions = + typeof urlOrOptions === "string" + ? { url: urlOrOptions, ...options } + : urlOrOptions; + + if (!body.url) throw new Error("SnapAPI: url is required"); + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.timeout); + + try { + const res = await fetch(`${this.baseUrl}/v1/screenshot`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify(body), + signal: controller.signal, + }); + + if (!res.ok) { + let detail = `HTTP ${res.status}`; + try { + const json = (await res.json()) as { error?: string }; + detail = json.error ?? detail; + } catch {} + throw new SnapAPIError(res.status, detail); + } + + const arrayBuffer = await res.arrayBuffer(); + return Buffer.from(arrayBuffer); + } finally { + clearTimeout(timer); + } + } + + /** + * Check API health status. + * + * @returns Health check response + * + * @example + * ```typescript + * const health = await snap.health(); + * console.log(health.status); // "ok" + * ``` + */ + async health(): Promise<{ + status: string; + version: string; + uptime: number; + browser: { + browsers: number; + totalPages: number; + availablePages: number; + queueDepth: number; + }; + }> { + const res = await fetch(`${this.baseUrl}/health`); + if (!res.ok) throw new SnapAPIError(res.status, "Health check failed"); + return res.json() as any; + } +} + +export default SnapAPI; diff --git a/sdk/node/tsconfig.json b/sdk/node/tsconfig.json new file mode 100644 index 0000000..6b973e7 --- /dev/null +++ b/sdk/node/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/sdk/python/README.md b/sdk/python/README.md new file mode 100644 index 0000000..c7bfed8 --- /dev/null +++ b/sdk/python/README.md @@ -0,0 +1,120 @@ +# SnapAPI Python SDK + +Official Python client for [SnapAPI](https://snapapi.eu) — the EU-hosted screenshot API. + +**Zero dependencies.** Uses only Python standard library (`urllib`). + +## Installation + +```bash +pip install snapapi +``` + +## Quick Start + +```python +from snapapi import SnapAPI + +snap = SnapAPI("your-api-key") + +# Capture a screenshot +screenshot = snap.capture("https://example.com") + +with open("screenshot.png", "wb") as f: + f.write(screenshot) +``` + +## Usage + +### Basic Screenshot + +```python +png = snap.capture("https://example.com") +``` + +### With Options + +```python +jpg = snap.capture( + "https://example.com", + format="jpeg", + width=1920, + height=1080, + quality=90, +) +``` + +### Full-Page Capture + +```python +full = snap.capture( + "https://example.com/blog", + full_page=True, + device_scale=2, # Retina +) +``` + +### Mobile Viewport + +```python +mobile = snap.capture( + "https://example.com", + width=375, + height=812, + device_scale=2, +) +``` + +### Wait for Dynamic Content + +```python +screenshot = snap.capture( + "https://example.com/dashboard", + wait_for_selector="#chart-loaded", + wait_until="networkidle2", +) +``` + +### Error Handling + +```python +from snapapi import SnapAPI, SnapAPIError + +snap = SnapAPI("your-api-key") + +try: + screenshot = snap.capture("https://example.com") +except SnapAPIError as e: + print(f"API error {e.status}: {e.detail}") +``` + +## API Reference + +### `SnapAPI(api_key, base_url="https://snapapi.eu", timeout=30)` + +### `snap.capture(url, **options) -> bytes` + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `url` | `str` | — | URL to capture (required) | +| `format` | `str` | `"png"` | Output: `png`, `jpeg`, `webp` | +| `width` | `int` | `1280` | Viewport width (320–3840) | +| `height` | `int` | `800` | Viewport height (200–2160) | +| `full_page` | `bool` | `False` | Capture full page | +| `quality` | `int` | `80` | JPEG/WebP quality (1–100) | +| `wait_for_selector` | `str` | — | CSS selector to wait for | +| `device_scale` | `float` | `1` | Device pixel ratio (1–3) | +| `delay` | `int` | `0` | Extra delay in ms (0–5000) | +| `wait_until` | `str` | `"domcontentloaded"` | Load event | + +### `snap.health() -> dict` + +Returns API health status. + +## EU-Hosted & GDPR Compliant + +SnapAPI runs entirely on EU infrastructure (Germany). Your data never leaves the EU. + +## License + +MIT — [Cloonar Technologies GmbH](https://snapapi.eu) diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml new file mode 100644 index 0000000..0bb01e9 --- /dev/null +++ b/sdk/python/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["setuptools>=68.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "snapapi" +version = "1.0.0" +description = "Official Python SDK for SnapAPI — EU-hosted screenshot API" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.8" +authors = [{name = "Cloonar Technologies GmbH"}] +keywords = ["screenshot", "api", "webpage", "capture", "eu", "gdpr"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Topic :: Internet :: WWW/HTTP", +] + +[project.urls] +Homepage = "https://snapapi.eu" +Repository = "https://git.cloonar.com/openclawd/SnapAPI" +Documentation = "https://snapapi.eu/docs" diff --git a/sdk/python/src/snapapi/__init__.py b/sdk/python/src/snapapi/__init__.py new file mode 100644 index 0000000..6956bd9 --- /dev/null +++ b/sdk/python/src/snapapi/__init__.py @@ -0,0 +1,6 @@ +"""SnapAPI Python SDK — EU-hosted screenshot API client.""" + +from .client import SnapAPI, SnapAPIError, ScreenshotOptions + +__all__ = ["SnapAPI", "SnapAPIError", "ScreenshotOptions"] +__version__ = "1.0.0" diff --git a/sdk/python/src/snapapi/client.py b/sdk/python/src/snapapi/client.py new file mode 100644 index 0000000..63983f9 --- /dev/null +++ b/sdk/python/src/snapapi/client.py @@ -0,0 +1,200 @@ +"""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())