feat: add Node.js and Python SDKs
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m1s
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
This commit is contained in:
parent
db1fa8d506
commit
66ecc471cf
8 changed files with 709 additions and 0 deletions
126
sdk/node/README.md
Normal file
126
sdk/node/README.md
Normal file
|
|
@ -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<Buffer>` 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)
|
||||||
34
sdk/node/package.json
Normal file
34
sdk/node/package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
184
sdk/node/src/index.ts
Normal file
184
sdk/node/src/index.ts
Normal file
|
|
@ -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<ScreenshotOptions, "url">
|
||||||
|
): Promise<Buffer> {
|
||||||
|
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;
|
||||||
14
sdk/node/tsconfig.json
Normal file
14
sdk/node/tsconfig.json
Normal file
|
|
@ -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"]
|
||||||
|
}
|
||||||
120
sdk/python/README.md
Normal file
120
sdk/python/README.md
Normal file
|
|
@ -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)
|
||||||
25
sdk/python/pyproject.toml
Normal file
25
sdk/python/pyproject.toml
Normal file
|
|
@ -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"
|
||||||
6
sdk/python/src/snapapi/__init__.py
Normal file
6
sdk/python/src/snapapi/__init__.py
Normal file
|
|
@ -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"
|
||||||
200
sdk/python/src/snapapi/client.py
Normal file
200
sdk/python/src/snapapi/client.py
Normal file
|
|
@ -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())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue