feat: add Node.js and Python SDKs
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:
OpenClawd 2026-02-23 14:02:15 +00:00
parent db1fa8d506
commit 66ecc471cf
8 changed files with 709 additions and 0 deletions

120
sdk/python/README.md Normal file
View 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 (3203840) |
| `height` | `int` | `800` | Viewport height (2002160) |
| `full_page` | `bool` | `False` | Capture full page |
| `quality` | `int` | `80` | JPEG/WebP quality (1100) |
| `wait_for_selector` | `str` | — | CSS selector to wait for |
| `device_scale` | `float` | `1` | Device pixel ratio (13) |
| `delay` | `int` | `0` | Extra delay in ms (05000) |
| `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
View 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"

View 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"

View 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())