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