rendex 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ __pycache__/
rendex-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,40 @@
1
+ # Rendex Python SDK
2
+
3
+ **Package**: `rendex` (PyPI)
4
+ **Parent**: Copperline Labs LLC
5
+ **Product**: Rendex (rendex.dev)
6
+
7
+ ## Overview
8
+
9
+ Official Python SDK for the Rendex screenshot API. Thin wrapper (~80 lines of core logic) around the REST API at api.rendex.dev. Single dependency: `httpx`.
10
+
11
+ ## Architecture
12
+
13
+ - `src/rendex/client.py` — `Rendex` class with 3 methods: `screenshot`, `screenshot_json`, `screenshot_url`
14
+ - `src/rendex/types.py` — TypedDict/dataclass definitions + camelCase conversion helpers
15
+ - `src/rendex/errors.py` — Exception hierarchy: `RendexError` → `RendexApiError` / `RendexNetworkError`
16
+ - `src/rendex/__init__.py` — Public exports
17
+ - `tests/test_client.py` — Unit tests with httpx MockTransport
18
+
19
+ ## Key Design Decisions
20
+
21
+ - **snake_case API**: Python-idiomatic (`full_page`, `dark_mode`), auto-converts to camelCase for the API.
22
+ - **httpx over requests**: Supports sync + async, HTTP/2, modern timeout handling.
23
+ - **Context manager**: `with Rendex("key") as r:` for connection reuse.
24
+ - **Positional url arg**: `rendex.screenshot("https://example.com")` reads naturally.
25
+
26
+ ## Development
27
+
28
+ ```bash
29
+ pip install -e ".[dev]" # Install in development mode
30
+ python -m pytest tests/ # Run tests
31
+ python -m mypy src/rendex/ # Type-check
32
+ ```
33
+
34
+ ## Publishing
35
+
36
+ Push a `sdk-py-v*` tag to trigger `.github/workflows/publish-sdk-python.yml`.
37
+
38
+ ## Type Sync
39
+
40
+ Options must match `ScreenshotRequestSchema` in `packages/screenshot-api/src/services/screenshot.ts`. If the API adds new parameters, update `src/rendex/types.py` (snake_case version).
rendex-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Copperline Labs LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rendex-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: rendex
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Rendex screenshot API — capture any webpage as an image
5
+ Project-URL: Homepage, https://rendex.dev
6
+ Project-URL: Documentation, https://rendex.dev/docs
7
+ Project-URL: Repository, https://github.com/copperline-labs/copperline-labs
8
+ Project-URL: Issues, https://github.com/copperline-labs/copperline-labs/issues
9
+ Author-email: Copperline Labs LLC <support@rendex.dev>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-agent,rendex,screenshot,screenshot-api,sdk,webpage-capture
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: httpx>=0.27
25
+ Description-Content-Type: text/markdown
26
+
27
+ # rendex
28
+
29
+ Official Python SDK for the [Rendex](https://rendex.dev) screenshot API. Capture any webpage as a high-quality image with a single function call.
30
+
31
+ - Full type hints (PEP 561 compatible)
32
+ - Single dependency (`httpx`)
33
+ - Sync API with context manager support
34
+ - Typed error handling with API error codes
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install rendex
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```python
45
+ from pathlib import Path
46
+ from rendex import Rendex
47
+
48
+ rendex = Rendex("your-api-key")
49
+
50
+ # Capture a screenshot (returns binary image)
51
+ result = rendex.screenshot("https://example.com", format="png", full_page=True)
52
+ Path("screenshot.png").write_bytes(result.image)
53
+
54
+ print(f"{result.metadata.bytes_size} bytes, loaded in {result.metadata.load_time_ms}ms")
55
+ ```
56
+
57
+ ## API Reference
58
+
59
+ ### `Rendex(api_key, *, base_url="https://api.rendex.dev")`
60
+
61
+ Create a new Rendex client.
62
+
63
+ ```python
64
+ rendex = Rendex("your-api-key")
65
+
66
+ # Or with context manager for connection reuse
67
+ with Rendex("your-api-key") as rendex:
68
+ result = rendex.screenshot("https://example.com")
69
+ ```
70
+
71
+ ### `rendex.screenshot(url, **options)`
72
+
73
+ Capture a screenshot and return the binary image with metadata.
74
+
75
+ ```python
76
+ result = rendex.screenshot(
77
+ "https://example.com",
78
+ format="webp",
79
+ width=1920,
80
+ height=1080,
81
+ dark_mode=True,
82
+ )
83
+ Path("screenshot.webp").write_bytes(result.image)
84
+ print(result.metadata.load_time_ms) # 350
85
+ ```
86
+
87
+ **Returns** `ScreenshotResult`:
88
+ - `image` — `bytes` of the captured image
89
+ - `metadata` — `ScreenshotMetadata` with url, dimensions, format, bytes_size, load_time_ms, quality, etc.
90
+
91
+ ### `rendex.screenshot_json(url, **options)`
92
+
93
+ Capture a screenshot and return JSON with a base64-encoded image.
94
+
95
+ ```python
96
+ result = rendex.screenshot_json("https://example.com")
97
+ print(result["data"]["bytesSize"]) # 45823
98
+ print(result["meta"]["usage"]["remaining"]) # 499
99
+ ```
100
+
101
+ **Returns** `ScreenshotJsonResponse` dict with `data` (image + metadata) and `meta` (request ID, usage).
102
+
103
+ ### `rendex.screenshot_url(url, **options)`
104
+
105
+ Generate a GET URL for embedding. No network call — pure URL builder.
106
+
107
+ ```python
108
+ url = rendex.screenshot_url("https://example.com", format="png", width=1200)
109
+ # Use in <img> tags, OpenGraph, etc.
110
+ ```
111
+
112
+ > **Note**: The API key is included in the URL. Use server-side only.
113
+
114
+ ### Screenshot Options
115
+
116
+ All options are keyword arguments in snake_case. Only `url` (positional) is required:
117
+
118
+ | Option | Type | Default | Description |
119
+ |--------|------|---------|-------------|
120
+ | `format` | `str` | `"png"` | `"png"`, `"jpeg"`, or `"webp"` |
121
+ | `width` | `int` | `1280` | Viewport width (320–3840) |
122
+ | `height` | `int` | `800` | Viewport height (240–2160) |
123
+ | `full_page` | `bool` | `False` | Capture the full scrollable page |
124
+ | `quality` | `int` | — | JPEG/WebP quality (1–100) |
125
+ | `delay` | `int` | `0` | Delay before capture in ms (0–10000) |
126
+ | `dark_mode` | `bool` | `False` | Emulate dark mode |
127
+ | `device_scale_factor` | `float` | `1` | Device pixel ratio (1–3) for Retina |
128
+ | `block_ads` | `bool` | `True` | Block ads and trackers |
129
+ | `block_resource_types` | `list` | — | Block: `"font"`, `"image"`, `"media"`, `"stylesheet"`, `"other"` |
130
+ | `timeout` | `int` | `30` | Page load timeout in seconds (5–60) |
131
+ | `wait_until` | `str` | `"networkidle2"` | `"load"`, `"domcontentloaded"`, `"networkidle0"`, `"networkidle2"` |
132
+ | `wait_for_selector` | `str` | — | CSS selector to wait for |
133
+ | `best_attempt` | `bool` | `True` | Return best-effort screenshot on timeout |
134
+ | `selector` | `str` | — | Capture a specific element by CSS selector |
135
+
136
+ ## Error Handling
137
+
138
+ ```python
139
+ from rendex import Rendex, RendexApiError, RendexNetworkError
140
+
141
+ rendex = Rendex("your-api-key")
142
+
143
+ try:
144
+ rendex.screenshot("https://example.com")
145
+ except RendexApiError as e:
146
+ # API returned an error
147
+ print(e.error_code) # "RATE_LIMITED", "VALIDATION_ERROR", etc.
148
+ print(e.status_code) # 429, 400, etc.
149
+ print(e.request_id) # For debugging with Rendex support
150
+ print(e.details) # Validation details (if any)
151
+ except RendexNetworkError as e:
152
+ # Network failure (DNS, timeout, connection refused)
153
+ print(f"Network error: {e}")
154
+ ```
155
+
156
+ ### Error Codes
157
+
158
+ | Code | HTTP Status | Description |
159
+ |------|-------------|-------------|
160
+ | `VALIDATION_ERROR` | 400 | Invalid request parameters |
161
+ | `INVALID_URL` | 400 | URL failed SSRF validation |
162
+ | `TIMEOUT` | 408 | Page took too long to load |
163
+ | `CAPTURE_FAILED` | 500 | Browser rendering error |
164
+ | `RATE_LIMITED` | 429 | Rate limit exceeded |
165
+ | `USAGE_EXCEEDED` | 429 | Monthly credit limit reached |
166
+ | `MISSING_API_KEY` | 401 | No API key provided |
167
+ | `INVALID_API_KEY` | 401 | API key verification failed |
168
+
169
+ ## License
170
+
171
+ MIT - [Copperline Labs LLC](https://copperlinelabs.com)
rendex-0.1.0/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # rendex
2
+
3
+ Official Python SDK for the [Rendex](https://rendex.dev) screenshot API. Capture any webpage as a high-quality image with a single function call.
4
+
5
+ - Full type hints (PEP 561 compatible)
6
+ - Single dependency (`httpx`)
7
+ - Sync API with context manager support
8
+ - Typed error handling with API error codes
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install rendex
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```python
19
+ from pathlib import Path
20
+ from rendex import Rendex
21
+
22
+ rendex = Rendex("your-api-key")
23
+
24
+ # Capture a screenshot (returns binary image)
25
+ result = rendex.screenshot("https://example.com", format="png", full_page=True)
26
+ Path("screenshot.png").write_bytes(result.image)
27
+
28
+ print(f"{result.metadata.bytes_size} bytes, loaded in {result.metadata.load_time_ms}ms")
29
+ ```
30
+
31
+ ## API Reference
32
+
33
+ ### `Rendex(api_key, *, base_url="https://api.rendex.dev")`
34
+
35
+ Create a new Rendex client.
36
+
37
+ ```python
38
+ rendex = Rendex("your-api-key")
39
+
40
+ # Or with context manager for connection reuse
41
+ with Rendex("your-api-key") as rendex:
42
+ result = rendex.screenshot("https://example.com")
43
+ ```
44
+
45
+ ### `rendex.screenshot(url, **options)`
46
+
47
+ Capture a screenshot and return the binary image with metadata.
48
+
49
+ ```python
50
+ result = rendex.screenshot(
51
+ "https://example.com",
52
+ format="webp",
53
+ width=1920,
54
+ height=1080,
55
+ dark_mode=True,
56
+ )
57
+ Path("screenshot.webp").write_bytes(result.image)
58
+ print(result.metadata.load_time_ms) # 350
59
+ ```
60
+
61
+ **Returns** `ScreenshotResult`:
62
+ - `image` — `bytes` of the captured image
63
+ - `metadata` — `ScreenshotMetadata` with url, dimensions, format, bytes_size, load_time_ms, quality, etc.
64
+
65
+ ### `rendex.screenshot_json(url, **options)`
66
+
67
+ Capture a screenshot and return JSON with a base64-encoded image.
68
+
69
+ ```python
70
+ result = rendex.screenshot_json("https://example.com")
71
+ print(result["data"]["bytesSize"]) # 45823
72
+ print(result["meta"]["usage"]["remaining"]) # 499
73
+ ```
74
+
75
+ **Returns** `ScreenshotJsonResponse` dict with `data` (image + metadata) and `meta` (request ID, usage).
76
+
77
+ ### `rendex.screenshot_url(url, **options)`
78
+
79
+ Generate a GET URL for embedding. No network call — pure URL builder.
80
+
81
+ ```python
82
+ url = rendex.screenshot_url("https://example.com", format="png", width=1200)
83
+ # Use in <img> tags, OpenGraph, etc.
84
+ ```
85
+
86
+ > **Note**: The API key is included in the URL. Use server-side only.
87
+
88
+ ### Screenshot Options
89
+
90
+ All options are keyword arguments in snake_case. Only `url` (positional) is required:
91
+
92
+ | Option | Type | Default | Description |
93
+ |--------|------|---------|-------------|
94
+ | `format` | `str` | `"png"` | `"png"`, `"jpeg"`, or `"webp"` |
95
+ | `width` | `int` | `1280` | Viewport width (320–3840) |
96
+ | `height` | `int` | `800` | Viewport height (240–2160) |
97
+ | `full_page` | `bool` | `False` | Capture the full scrollable page |
98
+ | `quality` | `int` | — | JPEG/WebP quality (1–100) |
99
+ | `delay` | `int` | `0` | Delay before capture in ms (0–10000) |
100
+ | `dark_mode` | `bool` | `False` | Emulate dark mode |
101
+ | `device_scale_factor` | `float` | `1` | Device pixel ratio (1–3) for Retina |
102
+ | `block_ads` | `bool` | `True` | Block ads and trackers |
103
+ | `block_resource_types` | `list` | — | Block: `"font"`, `"image"`, `"media"`, `"stylesheet"`, `"other"` |
104
+ | `timeout` | `int` | `30` | Page load timeout in seconds (5–60) |
105
+ | `wait_until` | `str` | `"networkidle2"` | `"load"`, `"domcontentloaded"`, `"networkidle0"`, `"networkidle2"` |
106
+ | `wait_for_selector` | `str` | — | CSS selector to wait for |
107
+ | `best_attempt` | `bool` | `True` | Return best-effort screenshot on timeout |
108
+ | `selector` | `str` | — | Capture a specific element by CSS selector |
109
+
110
+ ## Error Handling
111
+
112
+ ```python
113
+ from rendex import Rendex, RendexApiError, RendexNetworkError
114
+
115
+ rendex = Rendex("your-api-key")
116
+
117
+ try:
118
+ rendex.screenshot("https://example.com")
119
+ except RendexApiError as e:
120
+ # API returned an error
121
+ print(e.error_code) # "RATE_LIMITED", "VALIDATION_ERROR", etc.
122
+ print(e.status_code) # 429, 400, etc.
123
+ print(e.request_id) # For debugging with Rendex support
124
+ print(e.details) # Validation details (if any)
125
+ except RendexNetworkError as e:
126
+ # Network failure (DNS, timeout, connection refused)
127
+ print(f"Network error: {e}")
128
+ ```
129
+
130
+ ### Error Codes
131
+
132
+ | Code | HTTP Status | Description |
133
+ |------|-------------|-------------|
134
+ | `VALIDATION_ERROR` | 400 | Invalid request parameters |
135
+ | `INVALID_URL` | 400 | URL failed SSRF validation |
136
+ | `TIMEOUT` | 408 | Page took too long to load |
137
+ | `CAPTURE_FAILED` | 500 | Browser rendering error |
138
+ | `RATE_LIMITED` | 429 | Rate limit exceeded |
139
+ | `USAGE_EXCEEDED` | 429 | Monthly credit limit reached |
140
+ | `MISSING_API_KEY` | 401 | No API key provided |
141
+ | `INVALID_API_KEY` | 401 | API key verification failed |
142
+
143
+ ## License
144
+
145
+ MIT - [Copperline Labs LLC](https://copperlinelabs.com)
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@copperline/rendex-python",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Python SDK for Rendex — managed via pyproject.toml, not npm"
6
+ }
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "rendex"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the Rendex screenshot API — capture any webpage as an image"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Copperline Labs LLC", email = "support@rendex.dev" }]
13
+ keywords = ["rendex", "screenshot", "screenshot-api", "webpage-capture", "sdk", "ai-agent"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = ["httpx>=0.27"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://rendex.dev"
30
+ Documentation = "https://rendex.dev/docs"
31
+ Repository = "https://github.com/copperline-labs/copperline-labs"
32
+ Issues = "https://github.com/copperline-labs/copperline-labs/issues"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/rendex"]
@@ -0,0 +1,29 @@
1
+ """Rendex — Official Python SDK for the Rendex screenshot API."""
2
+
3
+ from .client import Rendex
4
+ from .errors import RendexApiError, RendexError, RendexNetworkError
5
+ from .types import (
6
+ ScreenshotData,
7
+ ScreenshotJsonResponse,
8
+ ScreenshotMetadata,
9
+ ScreenshotOptions,
10
+ ScreenshotResult,
11
+ ResponseMeta,
12
+ UsageInfo,
13
+ )
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ __all__ = [
18
+ "Rendex",
19
+ "RendexError",
20
+ "RendexApiError",
21
+ "RendexNetworkError",
22
+ "ScreenshotOptions",
23
+ "ScreenshotResult",
24
+ "ScreenshotMetadata",
25
+ "ScreenshotJsonResponse",
26
+ "ScreenshotData",
27
+ "ResponseMeta",
28
+ "UsageInfo",
29
+ ]
@@ -0,0 +1,187 @@
1
+ """Rendex SDK client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from urllib.parse import urlencode
7
+
8
+ import httpx
9
+
10
+ from .errors import RendexApiError, RendexError, RendexNetworkError
11
+ from .types import (
12
+ ScreenshotJsonResponse,
13
+ ScreenshotMetadata,
14
+ ScreenshotResult,
15
+ _build_body,
16
+ _to_camel,
17
+ )
18
+
19
+ DEFAULT_BASE_URL = "https://api.rendex.dev"
20
+
21
+
22
+ class Rendex:
23
+ """Official Python client for the Rendex screenshot API.
24
+
25
+ Usage::
26
+
27
+ from rendex import Rendex
28
+
29
+ rendex = Rendex("your-api-key")
30
+ result = rendex.screenshot("https://example.com", full_page=True)
31
+ Path("screenshot.png").write_bytes(result.image)
32
+
33
+ Or as a context manager::
34
+
35
+ with Rendex("your-api-key") as rendex:
36
+ result = rendex.screenshot("https://example.com")
37
+ """
38
+
39
+ def __init__(self, api_key: str, *, base_url: str = DEFAULT_BASE_URL) -> None:
40
+ if not api_key:
41
+ raise RendexError("API key is required. Get one at https://rendex.dev")
42
+ self._api_key = api_key
43
+ self._base_url = base_url.rstrip("/")
44
+ self._client = httpx.Client(
45
+ base_url=self._base_url,
46
+ headers={
47
+ "Authorization": f"Bearer {api_key}",
48
+ "Content-Type": "application/json",
49
+ },
50
+ timeout=90.0,
51
+ )
52
+
53
+ def screenshot(self, url: str, **options: Any) -> ScreenshotResult:
54
+ """Capture a screenshot and return the binary image with metadata.
55
+
56
+ Args:
57
+ url: The webpage URL to capture.
58
+ **options: Screenshot options (snake_case). See ScreenshotOptions.
59
+
60
+ Returns:
61
+ ScreenshotResult with ``image`` (bytes) and ``metadata``.
62
+ """
63
+ body = _build_body(url, options)
64
+ response = self._request("/v1/screenshot", body)
65
+
66
+ metadata = self._parse_metadata_headers(response.headers, len(response.content))
67
+
68
+ return ScreenshotResult(image=response.content, metadata=metadata)
69
+
70
+ def screenshot_json(self, url: str, **options: Any) -> ScreenshotJsonResponse:
71
+ """Capture a screenshot and return JSON with a base64-encoded image.
72
+
73
+ Args:
74
+ url: The webpage URL to capture.
75
+ **options: Screenshot options (snake_case). See ScreenshotOptions.
76
+
77
+ Returns:
78
+ ScreenshotJsonResponse with ``data`` and ``meta``.
79
+ """
80
+ body = _build_body(url, options)
81
+ response = self._request("/v1/screenshot/json", body)
82
+ data = response.json()
83
+
84
+ if not data.get("success"):
85
+ error = data.get("error", {})
86
+ meta = data.get("meta", {})
87
+ raise RendexApiError(
88
+ message=error.get("message", "Unknown error"),
89
+ status_code=response.status_code,
90
+ error_code=error.get("code", "UNKNOWN"),
91
+ request_id=meta.get("requestId"),
92
+ details=error.get("details"),
93
+ )
94
+
95
+ return data # type: ignore[return-value]
96
+
97
+ def screenshot_url(self, url: str, **options: Any) -> str:
98
+ """Generate a signed GET URL for embedding (no network call).
99
+
100
+ Useful for ``<img>`` tags, OpenGraph images, or anywhere you need a URL.
101
+
102
+ **Note**: The API key is included in the URL. Use server-side only.
103
+
104
+ Args:
105
+ url: The webpage URL to capture.
106
+ **options: Screenshot options (snake_case).
107
+
108
+ Returns:
109
+ A complete URL string to the screenshot endpoint.
110
+ """
111
+ params: list[tuple[str, str]] = [("key", self._api_key), ("url", url)]
112
+
113
+ for key, value in options.items():
114
+ if value is None:
115
+ continue
116
+ camel_key = _to_camel(key)
117
+ if isinstance(value, list):
118
+ for item in value:
119
+ params.append((camel_key, str(item)))
120
+ elif isinstance(value, bool):
121
+ params.append((camel_key, str(value).lower()))
122
+ else:
123
+ params.append((camel_key, str(value)))
124
+
125
+ return f"{self._base_url}/v1/screenshot?{urlencode(params)}"
126
+
127
+ def close(self) -> None:
128
+ """Close the underlying HTTP client."""
129
+ self._client.close()
130
+
131
+ def __enter__(self) -> Rendex:
132
+ return self
133
+
134
+ def __exit__(self, *args: Any) -> None:
135
+ self.close()
136
+
137
+ # ─── Private ─────────────────────────────────────────────────────
138
+
139
+ def _request(self, path: str, body: dict[str, Any]) -> httpx.Response:
140
+ try:
141
+ response = self._client.post(path, json=body)
142
+ except httpx.HTTPError as exc:
143
+ raise RendexNetworkError(
144
+ f"Network request to {self._base_url}{path} failed: {exc}",
145
+ cause=exc,
146
+ ) from exc
147
+
148
+ if response.status_code >= 400:
149
+ self._handle_error_response(response)
150
+
151
+ return response
152
+
153
+ def _handle_error_response(self, response: httpx.Response) -> None:
154
+ try:
155
+ data = response.json()
156
+ except Exception:
157
+ raise RendexApiError(
158
+ message=f"HTTP {response.status_code}",
159
+ status_code=response.status_code,
160
+ error_code="UNKNOWN",
161
+ )
162
+
163
+ error = data.get("error", {})
164
+ meta = data.get("meta", {})
165
+
166
+ raise RendexApiError(
167
+ message=error.get("message", f"HTTP {response.status_code}"),
168
+ status_code=response.status_code,
169
+ error_code=error.get("code", "UNKNOWN"),
170
+ request_id=meta.get("requestId"),
171
+ details=error.get("details"),
172
+ )
173
+
174
+ @staticmethod
175
+ def _parse_metadata_headers(headers: httpx.Headers, fallback_size: int) -> ScreenshotMetadata:
176
+ return ScreenshotMetadata(
177
+ url=headers.get("x-screenshot-url", ""),
178
+ width=int(headers.get("x-screenshot-width", "0")),
179
+ height=int(headers.get("x-screenshot-height", "0")),
180
+ format=headers.get("x-rendex-format", "png"),
181
+ bytes_size=int(headers.get("x-screenshot-size", str(fallback_size))),
182
+ captured_at=headers.get("x-screenshot-captured-at", ""),
183
+ quality=headers.get("x-rendex-quality", "full"),
184
+ wait_strategy=headers.get("x-rendex-wait-strategy", "unknown"),
185
+ load_time_ms=int(headers.get("x-rendex-load-time-ms", "0")),
186
+ truncated=headers.get("x-rendex-truncated") == "true",
187
+ )
@@ -0,0 +1,47 @@
1
+ """Rendex SDK error classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+
8
+ class RendexError(Exception):
9
+ """Base error for all Rendex SDK errors."""
10
+
11
+ pass
12
+
13
+
14
+ class RendexApiError(RendexError):
15
+ """Error returned by the Rendex API (non-2xx response with a parsed body)."""
16
+
17
+ def __init__(
18
+ self,
19
+ message: str,
20
+ status_code: int,
21
+ error_code: str,
22
+ request_id: Optional[str] = None,
23
+ details: Any = None,
24
+ ) -> None:
25
+ super().__init__(message)
26
+ self.status_code = status_code
27
+ """HTTP status code (e.g. 400, 401, 429, 500)."""
28
+ self.error_code = error_code
29
+ """API error code (e.g. "RATE_LIMITED", "VALIDATION_ERROR")."""
30
+ self.request_id = request_id
31
+ """Unique request ID for debugging with Rendex support."""
32
+ self.details = details
33
+ """Additional error details (e.g. validation field errors)."""
34
+
35
+ def __str__(self) -> str:
36
+ parts = [f"[{self.error_code}] {super().__str__()}"]
37
+ if self.request_id:
38
+ parts.append(f"(request: {self.request_id})")
39
+ return " ".join(parts)
40
+
41
+
42
+ class RendexNetworkError(RendexError):
43
+ """Network-level error (DNS failure, timeout, connection refused, etc.)."""
44
+
45
+ def __init__(self, message: str, cause: Optional[Exception] = None) -> None:
46
+ super().__init__(message)
47
+ self.__cause__ = cause
File without changes
@@ -0,0 +1,102 @@
1
+ """Rendex SDK type definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, List, Literal, Optional, TypedDict
7
+
8
+
9
+ class ScreenshotOptions(TypedDict, total=False):
10
+ """Options for capturing a screenshot. Only ``url`` is required."""
11
+
12
+ url: str # Required — set via positional arg in client methods
13
+ format: Literal["png", "jpeg", "webp"]
14
+ width: int
15
+ height: int
16
+ full_page: bool
17
+ quality: int
18
+ delay: int
19
+ dark_mode: bool
20
+ device_scale_factor: float
21
+ block_ads: bool
22
+ block_resource_types: List[Literal["font", "image", "media", "stylesheet", "other"]]
23
+ timeout: int
24
+ wait_until: Literal["load", "domcontentloaded", "networkidle0", "networkidle2"]
25
+ wait_for_selector: str
26
+ best_attempt: bool
27
+ selector: str
28
+
29
+
30
+ class UsageInfo(TypedDict):
31
+ credits: int
32
+ remaining: int
33
+
34
+
35
+ class ResponseMeta(TypedDict, total=False):
36
+ requestId: str
37
+ timestamp: str
38
+ usage: UsageInfo
39
+
40
+
41
+ class ScreenshotData(TypedDict, total=False):
42
+ image: str # base64
43
+ contentType: str
44
+ url: str
45
+ width: int
46
+ height: int
47
+ format: str
48
+ bytesSize: int
49
+ capturedAt: str
50
+ quality: Literal["full", "degraded", "best_attempt"]
51
+ waitStrategy: str
52
+ loadTimeMs: int
53
+ truncated: bool
54
+
55
+
56
+ class ScreenshotJsonResponse(TypedDict):
57
+ success: bool
58
+ data: ScreenshotData
59
+ meta: ResponseMeta
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ScreenshotMetadata:
64
+ """Metadata from response headers for binary screenshots."""
65
+
66
+ url: str
67
+ width: int
68
+ height: int
69
+ format: str
70
+ bytes_size: int
71
+ captured_at: str
72
+ quality: str # "full", "degraded", or "best_attempt"
73
+ wait_strategy: str
74
+ load_time_ms: int
75
+ truncated: bool
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class ScreenshotResult:
80
+ """Result from ``Rendex.screenshot()`` — binary image with metadata."""
81
+
82
+ image: bytes
83
+ """Raw image bytes. Write to file, upload to S3, etc."""
84
+
85
+ metadata: ScreenshotMetadata
86
+ """Capture metadata extracted from response headers."""
87
+
88
+
89
+ # camelCase conversion for API requests
90
+ def _to_camel(key: str) -> str:
91
+ """Convert snake_case to camelCase."""
92
+ parts = key.split("_")
93
+ return parts[0] + "".join(p.capitalize() for p in parts[1:])
94
+
95
+
96
+ def _build_body(url: str, options: Any) -> dict[str, Any]:
97
+ """Build the JSON request body from url + snake_case kwargs."""
98
+ body: dict[str, Any] = {"url": url}
99
+ for key, value in options.items():
100
+ if value is not None:
101
+ body[_to_camel(key)] = value
102
+ return body
@@ -0,0 +1,239 @@
1
+ """Tests for the Rendex Python SDK."""
2
+
3
+ import json
4
+ from unittest.mock import MagicMock
5
+
6
+ import httpx
7
+ import pytest
8
+
9
+ from rendex import Rendex, RendexApiError, RendexError, RendexNetworkError
10
+
11
+
12
+ # ─── Constructor ──────────────────────────────────────────────────────
13
+
14
+
15
+ def test_missing_api_key():
16
+ with pytest.raises(RendexError, match="API key is required"):
17
+ Rendex("")
18
+
19
+
20
+ def test_custom_base_url():
21
+ client = Rendex("test-key", base_url="https://custom.api.dev/")
22
+ assert client._base_url == "https://custom.api.dev"
23
+ client.close()
24
+
25
+
26
+ # ─── screenshot_url ───────────────────────────────────────────────────
27
+
28
+
29
+ def test_screenshot_url_basic():
30
+ client = Rendex("test-key")
31
+ url = client.screenshot_url("https://example.com")
32
+ assert "key=test-key" in url
33
+ assert "url=https" in url
34
+ assert url.startswith("https://api.rendex.dev/v1/screenshot?")
35
+ client.close()
36
+
37
+
38
+ def test_screenshot_url_with_options():
39
+ client = Rendex("test-key")
40
+ url = client.screenshot_url(
41
+ "https://example.com",
42
+ format="webp",
43
+ full_page=True,
44
+ width=1920,
45
+ )
46
+ assert "format=webp" in url
47
+ assert "fullPage=true" in url
48
+ assert "width=1920" in url
49
+ client.close()
50
+
51
+
52
+ def test_screenshot_url_array_params():
53
+ client = Rendex("test-key")
54
+ url = client.screenshot_url(
55
+ "https://example.com",
56
+ block_resource_types=["font", "image"],
57
+ )
58
+ assert "blockResourceTypes=font" in url
59
+ assert "blockResourceTypes=image" in url
60
+ client.close()
61
+
62
+
63
+ # ─── camelCase conversion ────────────────────────────────────────────
64
+
65
+
66
+ def test_to_camel():
67
+ from rendex.types import _to_camel
68
+
69
+ assert _to_camel("full_page") == "fullPage"
70
+ assert _to_camel("dark_mode") == "darkMode"
71
+ assert _to_camel("device_scale_factor") == "deviceScaleFactor"
72
+ assert _to_camel("url") == "url"
73
+ assert _to_camel("block_resource_types") == "blockResourceTypes"
74
+
75
+
76
+ def test_build_body():
77
+ from rendex.types import _build_body
78
+
79
+ body = _build_body("https://example.com", {"full_page": True, "dark_mode": False, "quality": None})
80
+ assert body == {"url": "https://example.com", "fullPage": True, "darkMode": False}
81
+
82
+
83
+ # ─── Error handling ──────────────────────────────────────────────────
84
+
85
+
86
+ def test_api_error_str():
87
+ err = RendexApiError(
88
+ message="Rate limit exceeded",
89
+ status_code=429,
90
+ error_code="RATE_LIMITED",
91
+ request_id="req-123",
92
+ )
93
+ assert "[RATE_LIMITED]" in str(err)
94
+ assert "req-123" in str(err)
95
+ assert err.status_code == 429
96
+ assert err.error_code == "RATE_LIMITED"
97
+
98
+
99
+ def test_api_error_no_request_id():
100
+ err = RendexApiError(
101
+ message="Bad request",
102
+ status_code=400,
103
+ error_code="VALIDATION_ERROR",
104
+ )
105
+ assert "[VALIDATION_ERROR]" in str(err)
106
+ assert "request:" not in str(err)
107
+
108
+
109
+ # ─── Context manager ────────────────────────────────────────────────
110
+
111
+
112
+ def test_context_manager():
113
+ with Rendex("test-key") as client:
114
+ assert client._api_key == "test-key"
115
+ # Should not raise after close
116
+
117
+
118
+ # ─── Mocked HTTP tests ──────────────────────────────────────────────
119
+
120
+
121
+ def test_screenshot_json_success():
122
+ """Test screenshotJson with a mocked transport."""
123
+ mock_response_data = {
124
+ "success": True,
125
+ "data": {
126
+ "image": "iVBORw0KGgo=",
127
+ "contentType": "image/png",
128
+ "url": "https://example.com",
129
+ "width": 1280,
130
+ "height": 800,
131
+ "format": "png",
132
+ "bytesSize": 1234,
133
+ "capturedAt": "2026-04-02T00:00:00Z",
134
+ "quality": "full",
135
+ "waitStrategy": "networkidle2",
136
+ "loadTimeMs": 500,
137
+ },
138
+ "meta": {
139
+ "requestId": "req-abc",
140
+ "timestamp": "2026-04-02T00:00:00Z",
141
+ "usage": {"credits": 1, "remaining": 499},
142
+ },
143
+ }
144
+
145
+ transport = httpx.MockTransport(
146
+ lambda request: httpx.Response(
147
+ 200,
148
+ json=mock_response_data,
149
+ )
150
+ )
151
+
152
+ client = Rendex("test-key")
153
+ client._client = httpx.Client(
154
+ base_url="https://api.rendex.dev",
155
+ headers={"Authorization": "Bearer test-key", "Content-Type": "application/json"},
156
+ transport=transport,
157
+ timeout=90.0,
158
+ )
159
+
160
+ result = client.screenshot_json("https://example.com")
161
+ assert result["success"] is True
162
+ assert result["data"]["bytesSize"] == 1234
163
+ assert result["meta"]["usage"]["remaining"] == 499
164
+ client.close()
165
+
166
+
167
+ def test_screenshot_binary_success():
168
+ """Test screenshot with mocked binary response + headers."""
169
+ fake_image = b"\x89PNG\r\n\x1a\n" + b"\x00" * 100
170
+
171
+ transport = httpx.MockTransport(
172
+ lambda request: httpx.Response(
173
+ 200,
174
+ content=fake_image,
175
+ headers={
176
+ "content-type": "image/png",
177
+ "x-screenshot-url": "https://example.com",
178
+ "x-screenshot-width": "1280",
179
+ "x-screenshot-height": "800",
180
+ "x-screenshot-size": str(len(fake_image)),
181
+ "x-screenshot-captured-at": "2026-04-02T00:00:00Z",
182
+ "x-rendex-format": "png",
183
+ "x-rendex-quality": "full",
184
+ "x-rendex-wait-strategy": "networkidle2",
185
+ "x-rendex-load-time-ms": "350",
186
+ },
187
+ )
188
+ )
189
+
190
+ client = Rendex("test-key")
191
+ client._client = httpx.Client(
192
+ base_url="https://api.rendex.dev",
193
+ headers={"Authorization": "Bearer test-key", "Content-Type": "application/json"},
194
+ transport=transport,
195
+ timeout=90.0,
196
+ )
197
+
198
+ result = client.screenshot("https://example.com")
199
+ assert result.image == fake_image
200
+ assert result.metadata.width == 1280
201
+ assert result.metadata.format == "png"
202
+ assert result.metadata.load_time_ms == 350
203
+ assert result.metadata.truncated is False
204
+ client.close()
205
+
206
+
207
+ def test_api_error_response():
208
+ """Test that API errors are properly parsed."""
209
+ error_body = {
210
+ "success": False,
211
+ "error": {
212
+ "code": "RATE_LIMITED",
213
+ "message": "Rate limit exceeded",
214
+ },
215
+ "meta": {
216
+ "requestId": "req-xyz",
217
+ "timestamp": "2026-04-02T00:00:00Z",
218
+ },
219
+ }
220
+
221
+ transport = httpx.MockTransport(
222
+ lambda request: httpx.Response(429, json=error_body)
223
+ )
224
+
225
+ client = Rendex("test-key")
226
+ client._client = httpx.Client(
227
+ base_url="https://api.rendex.dev",
228
+ headers={"Authorization": "Bearer test-key", "Content-Type": "application/json"},
229
+ transport=transport,
230
+ timeout=90.0,
231
+ )
232
+
233
+ with pytest.raises(RendexApiError) as exc_info:
234
+ client.screenshot("https://example.com")
235
+
236
+ assert exc_info.value.status_code == 429
237
+ assert exc_info.value.error_code == "RATE_LIMITED"
238
+ assert exc_info.value.request_id == "req-xyz"
239
+ client.close()