snaprender 0.2.4__tar.gz → 0.4.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.
- {snaprender-0.2.4 → snaprender-0.4.0}/PKG-INFO +1 -1
- {snaprender-0.2.4 → snaprender-0.4.0}/pyproject.toml +1 -1
- {snaprender-0.2.4 → snaprender-0.4.0}/snaprender/__init__.py +1 -1
- snaprender-0.4.0/snaprender/client.py +284 -0
- snaprender-0.2.4/snaprender/client.py +0 -171
- {snaprender-0.2.4 → snaprender-0.4.0}/.gitignore +0 -0
- {snaprender-0.2.4 → snaprender-0.4.0}/LICENSE +0 -0
- {snaprender-0.2.4 → snaprender-0.4.0}/README.md +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""SnapRender API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SnapRenderError(Exception):
|
|
11
|
+
"""Error returned by the SnapRender API."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str, code: str = "UNKNOWN", status: int = 0):
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.code = code
|
|
16
|
+
self.status = status
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SnapRender:
|
|
20
|
+
"""SnapRender Screenshot API client.
|
|
21
|
+
|
|
22
|
+
Usage::
|
|
23
|
+
|
|
24
|
+
snap = SnapRender(api_key="sk_live_...")
|
|
25
|
+
image = snap.capture("https://example.com")
|
|
26
|
+
with open("screenshot.png", "wb") as f:
|
|
27
|
+
f.write(image)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
api_key: str,
|
|
33
|
+
base_url: str = "https://app.snap-render.com",
|
|
34
|
+
timeout: float = 60.0,
|
|
35
|
+
):
|
|
36
|
+
if not api_key:
|
|
37
|
+
raise ValueError("api_key is required")
|
|
38
|
+
self._base_url = base_url.rstrip("/")
|
|
39
|
+
self._client = httpx.Client(
|
|
40
|
+
base_url=self._base_url,
|
|
41
|
+
headers={"X-API-Key": api_key},
|
|
42
|
+
timeout=timeout,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def capture(
|
|
46
|
+
self,
|
|
47
|
+
url: Optional[str] = None,
|
|
48
|
+
*,
|
|
49
|
+
html: Optional[str] = None,
|
|
50
|
+
markdown: Optional[str] = None,
|
|
51
|
+
format: str = "png",
|
|
52
|
+
width: Optional[int] = None,
|
|
53
|
+
height: Optional[int] = None,
|
|
54
|
+
full_page: Optional[bool] = None,
|
|
55
|
+
quality: Optional[int] = None,
|
|
56
|
+
delay: Optional[int] = None,
|
|
57
|
+
dark_mode: Optional[bool] = None,
|
|
58
|
+
block_ads: Optional[bool] = None,
|
|
59
|
+
block_cookie_banners: Optional[bool] = None,
|
|
60
|
+
hide_selectors: Optional[str] = None,
|
|
61
|
+
click_selector: Optional[str] = None,
|
|
62
|
+
device: Optional[str] = None,
|
|
63
|
+
user_agent: Optional[str] = None,
|
|
64
|
+
cache: Optional[bool] = None,
|
|
65
|
+
cache_ttl: Optional[int] = None,
|
|
66
|
+
response_type: Optional[str] = None,
|
|
67
|
+
) -> "bytes | dict[str, Any]":
|
|
68
|
+
"""Capture a screenshot. Provide exactly one of: url, html, or markdown.
|
|
69
|
+
|
|
70
|
+
When html or markdown is provided, uses POST with JSON body.
|
|
71
|
+
When url is provided, uses GET with query parameters (backward compatible).
|
|
72
|
+
Returns bytes by default, or a dict when response_type='json'.
|
|
73
|
+
"""
|
|
74
|
+
use_post = bool(html or markdown)
|
|
75
|
+
|
|
76
|
+
if use_post:
|
|
77
|
+
body: dict[str, Any] = {"format": format}
|
|
78
|
+
if url is not None:
|
|
79
|
+
body["url"] = url
|
|
80
|
+
if html is not None:
|
|
81
|
+
body["html"] = html
|
|
82
|
+
if markdown is not None:
|
|
83
|
+
body["markdown"] = markdown
|
|
84
|
+
if width is not None:
|
|
85
|
+
body["width"] = width
|
|
86
|
+
if height is not None:
|
|
87
|
+
body["height"] = height
|
|
88
|
+
if full_page is not None:
|
|
89
|
+
body["full_page"] = full_page
|
|
90
|
+
if quality is not None:
|
|
91
|
+
body["quality"] = quality
|
|
92
|
+
if delay is not None:
|
|
93
|
+
body["delay"] = delay
|
|
94
|
+
if dark_mode is not None:
|
|
95
|
+
body["dark_mode"] = dark_mode
|
|
96
|
+
if block_ads is not None:
|
|
97
|
+
body["block_ads"] = block_ads
|
|
98
|
+
if block_cookie_banners is not None:
|
|
99
|
+
body["block_cookie_banners"] = block_cookie_banners
|
|
100
|
+
if hide_selectors is not None:
|
|
101
|
+
body["hide_selectors"] = hide_selectors
|
|
102
|
+
if click_selector is not None:
|
|
103
|
+
body["click_selector"] = click_selector
|
|
104
|
+
if device is not None:
|
|
105
|
+
body["device"] = device
|
|
106
|
+
if user_agent is not None:
|
|
107
|
+
body["user_agent"] = user_agent
|
|
108
|
+
if cache is not None:
|
|
109
|
+
body["cache"] = cache
|
|
110
|
+
if cache_ttl is not None:
|
|
111
|
+
body["cache_ttl"] = cache_ttl
|
|
112
|
+
if response_type is not None:
|
|
113
|
+
body["response_type"] = response_type
|
|
114
|
+
|
|
115
|
+
resp = self._client.post("/v1/screenshot", json=body)
|
|
116
|
+
else:
|
|
117
|
+
if not url:
|
|
118
|
+
raise ValueError("One of url, html, or markdown is required")
|
|
119
|
+
params: dict[str, Any] = {"url": url, "format": format}
|
|
120
|
+
if width is not None:
|
|
121
|
+
params["width"] = width
|
|
122
|
+
if height is not None:
|
|
123
|
+
params["height"] = height
|
|
124
|
+
if full_page is not None:
|
|
125
|
+
params["full_page"] = str(full_page).lower()
|
|
126
|
+
if quality is not None:
|
|
127
|
+
params["quality"] = quality
|
|
128
|
+
if delay is not None:
|
|
129
|
+
params["delay"] = delay
|
|
130
|
+
if dark_mode is not None:
|
|
131
|
+
params["dark_mode"] = str(dark_mode).lower()
|
|
132
|
+
if block_ads is not None:
|
|
133
|
+
params["block_ads"] = str(block_ads).lower()
|
|
134
|
+
if block_cookie_banners is not None:
|
|
135
|
+
params["block_cookie_banners"] = str(block_cookie_banners).lower()
|
|
136
|
+
if hide_selectors is not None:
|
|
137
|
+
params["hide_selectors"] = hide_selectors
|
|
138
|
+
if click_selector is not None:
|
|
139
|
+
params["click_selector"] = click_selector
|
|
140
|
+
if device is not None:
|
|
141
|
+
params["device"] = device
|
|
142
|
+
if user_agent is not None:
|
|
143
|
+
params["user_agent"] = user_agent
|
|
144
|
+
if cache is not None:
|
|
145
|
+
params["cache"] = str(cache).lower()
|
|
146
|
+
if cache_ttl is not None:
|
|
147
|
+
params["cache_ttl"] = cache_ttl
|
|
148
|
+
if response_type is not None:
|
|
149
|
+
params["response_type"] = response_type
|
|
150
|
+
|
|
151
|
+
resp = self._client.get("/v1/screenshot", params=params)
|
|
152
|
+
|
|
153
|
+
if resp.status_code != 200:
|
|
154
|
+
self._raise_error(resp)
|
|
155
|
+
if response_type == "json":
|
|
156
|
+
return resp.json()
|
|
157
|
+
return resp.content
|
|
158
|
+
|
|
159
|
+
def sign(
|
|
160
|
+
self,
|
|
161
|
+
url: str,
|
|
162
|
+
*,
|
|
163
|
+
expires_in: Optional[int] = None,
|
|
164
|
+
format: Optional[str] = None,
|
|
165
|
+
width: Optional[int] = None,
|
|
166
|
+
height: Optional[int] = None,
|
|
167
|
+
full_page: Optional[bool] = None,
|
|
168
|
+
quality: Optional[int] = None,
|
|
169
|
+
delay: Optional[int] = None,
|
|
170
|
+
dark_mode: Optional[bool] = None,
|
|
171
|
+
block_ads: Optional[bool] = None,
|
|
172
|
+
block_cookie_banners: Optional[bool] = None,
|
|
173
|
+
hide_selectors: Optional[str] = None,
|
|
174
|
+
click_selector: Optional[str] = None,
|
|
175
|
+
device: Optional[str] = None,
|
|
176
|
+
user_agent: Optional[str] = None,
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
"""Generate a signed URL that can be used without an API key.
|
|
179
|
+
|
|
180
|
+
Signing is free and does not count against your quota.
|
|
181
|
+
The signed URL consumes one credit when rendered.
|
|
182
|
+
Returns a dict with keys: signed_url, expires_at, expires_in.
|
|
183
|
+
"""
|
|
184
|
+
body: dict[str, Any] = {"url": url}
|
|
185
|
+
if expires_in is not None:
|
|
186
|
+
body["expires_in"] = expires_in
|
|
187
|
+
if format is not None:
|
|
188
|
+
body["format"] = format
|
|
189
|
+
if width is not None:
|
|
190
|
+
body["width"] = width
|
|
191
|
+
if height is not None:
|
|
192
|
+
body["height"] = height
|
|
193
|
+
if full_page is not None:
|
|
194
|
+
body["full_page"] = full_page
|
|
195
|
+
if quality is not None:
|
|
196
|
+
body["quality"] = quality
|
|
197
|
+
if delay is not None:
|
|
198
|
+
body["delay"] = delay
|
|
199
|
+
if dark_mode is not None:
|
|
200
|
+
body["dark_mode"] = dark_mode
|
|
201
|
+
if block_ads is not None:
|
|
202
|
+
body["block_ads"] = block_ads
|
|
203
|
+
if block_cookie_banners is not None:
|
|
204
|
+
body["block_cookie_banners"] = block_cookie_banners
|
|
205
|
+
if hide_selectors is not None:
|
|
206
|
+
body["hide_selectors"] = hide_selectors
|
|
207
|
+
if click_selector is not None:
|
|
208
|
+
body["click_selector"] = click_selector
|
|
209
|
+
if device is not None:
|
|
210
|
+
body["device"] = device
|
|
211
|
+
if user_agent is not None:
|
|
212
|
+
body["user_agent"] = user_agent
|
|
213
|
+
|
|
214
|
+
resp = self._client.post("/v1/screenshot/sign", json=body)
|
|
215
|
+
if resp.status_code != 200:
|
|
216
|
+
self._raise_error(resp)
|
|
217
|
+
return resp.json()
|
|
218
|
+
|
|
219
|
+
def info(self, url: str, **kwargs: Any) -> dict[str, Any]:
|
|
220
|
+
"""Get cache info for a URL without capturing.
|
|
221
|
+
|
|
222
|
+
Returns a dict with keys: url, cached, cache_key, cached_at,
|
|
223
|
+
expires_at, content_type.
|
|
224
|
+
"""
|
|
225
|
+
params: dict[str, Any] = {"url": url, **kwargs}
|
|
226
|
+
resp = self._client.get("/v1/screenshot/info", params=params)
|
|
227
|
+
if resp.status_code != 200:
|
|
228
|
+
self._raise_error(resp)
|
|
229
|
+
data = resp.json()
|
|
230
|
+
return {
|
|
231
|
+
"url": data["url"],
|
|
232
|
+
"cached": data["cached"],
|
|
233
|
+
"cache_key": data.get("cache_key"),
|
|
234
|
+
"cached_at": data.get("cached_at"),
|
|
235
|
+
"expires_at": data.get("expires_at"),
|
|
236
|
+
"content_type": data.get("content_type"),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def usage(self) -> dict[str, Any]:
|
|
240
|
+
"""Get current month's usage.
|
|
241
|
+
|
|
242
|
+
Returns a dict with keys: plan, used, limit, remaining, period.
|
|
243
|
+
"""
|
|
244
|
+
resp = self._client.get("/v1/usage")
|
|
245
|
+
if resp.status_code != 200:
|
|
246
|
+
self._raise_error(resp)
|
|
247
|
+
data = resp.json()
|
|
248
|
+
return {
|
|
249
|
+
"plan": data["plan"],
|
|
250
|
+
"used": data["usage"]["screenshots_used"],
|
|
251
|
+
"limit": data["usage"]["screenshots_limit"],
|
|
252
|
+
"remaining": data["usage"]["screenshots_remaining"],
|
|
253
|
+
"period": data["period"],
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
def usage_daily(self, days: int = 30) -> dict[str, Any]:
|
|
257
|
+
"""Get daily usage breakdown."""
|
|
258
|
+
resp = self._client.get("/v1/usage/daily", params={"days": days})
|
|
259
|
+
if resp.status_code != 200:
|
|
260
|
+
self._raise_error(resp)
|
|
261
|
+
return resp.json()
|
|
262
|
+
|
|
263
|
+
def close(self) -> None:
|
|
264
|
+
"""Close the HTTP client."""
|
|
265
|
+
self._client.close()
|
|
266
|
+
|
|
267
|
+
def __enter__(self) -> SnapRender:
|
|
268
|
+
return self
|
|
269
|
+
|
|
270
|
+
def __exit__(self, *args: Any) -> None:
|
|
271
|
+
self.close()
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def _raise_error(resp: httpx.Response) -> None:
|
|
275
|
+
try:
|
|
276
|
+
body = resp.json()
|
|
277
|
+
err = body.get("error", {})
|
|
278
|
+
raise SnapRenderError(
|
|
279
|
+
err.get("message", f"HTTP {resp.status_code}"),
|
|
280
|
+
err.get("code", "UNKNOWN"),
|
|
281
|
+
resp.status_code,
|
|
282
|
+
)
|
|
283
|
+
except (ValueError, KeyError):
|
|
284
|
+
raise SnapRenderError(f"HTTP {resp.status_code}", "UNKNOWN", resp.status_code)
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
"""SnapRender API client."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any, Optional
|
|
6
|
-
|
|
7
|
-
import httpx
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class SnapRenderError(Exception):
|
|
11
|
-
"""Error returned by the SnapRender API."""
|
|
12
|
-
|
|
13
|
-
def __init__(self, message: str, code: str = "UNKNOWN", status: int = 0):
|
|
14
|
-
super().__init__(message)
|
|
15
|
-
self.code = code
|
|
16
|
-
self.status = status
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class SnapRender:
|
|
20
|
-
"""SnapRender Screenshot API client.
|
|
21
|
-
|
|
22
|
-
Usage::
|
|
23
|
-
|
|
24
|
-
snap = SnapRender(api_key="sk_live_...")
|
|
25
|
-
image = snap.capture("https://example.com")
|
|
26
|
-
with open("screenshot.png", "wb") as f:
|
|
27
|
-
f.write(image)
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(
|
|
31
|
-
self,
|
|
32
|
-
api_key: str,
|
|
33
|
-
base_url: str = "https://app.snap-render.com",
|
|
34
|
-
timeout: float = 60.0,
|
|
35
|
-
):
|
|
36
|
-
if not api_key:
|
|
37
|
-
raise ValueError("api_key is required")
|
|
38
|
-
self._base_url = base_url.rstrip("/")
|
|
39
|
-
self._client = httpx.Client(
|
|
40
|
-
base_url=self._base_url,
|
|
41
|
-
headers={"X-API-Key": api_key},
|
|
42
|
-
timeout=timeout,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
def capture(
|
|
46
|
-
self,
|
|
47
|
-
url: str,
|
|
48
|
-
*,
|
|
49
|
-
format: str = "png",
|
|
50
|
-
width: Optional[int] = None,
|
|
51
|
-
height: Optional[int] = None,
|
|
52
|
-
full_page: Optional[bool] = None,
|
|
53
|
-
quality: Optional[int] = None,
|
|
54
|
-
delay: Optional[int] = None,
|
|
55
|
-
dark_mode: Optional[bool] = None,
|
|
56
|
-
block_ads: Optional[bool] = None,
|
|
57
|
-
block_cookie_banners: Optional[bool] = None,
|
|
58
|
-
hide_selectors: Optional[str] = None,
|
|
59
|
-
click_selector: Optional[str] = None,
|
|
60
|
-
device: Optional[str] = None,
|
|
61
|
-
user_agent: Optional[str] = None,
|
|
62
|
-
cache: Optional[bool] = None,
|
|
63
|
-
cache_ttl: Optional[int] = None,
|
|
64
|
-
response_type: Optional[str] = None,
|
|
65
|
-
) -> "bytes | dict[str, Any]":
|
|
66
|
-
"""Capture a screenshot. Returns bytes by default, or a dict when response_type='json'."""
|
|
67
|
-
params: dict[str, Any] = {"url": url, "format": format}
|
|
68
|
-
if width is not None:
|
|
69
|
-
params["width"] = width
|
|
70
|
-
if height is not None:
|
|
71
|
-
params["height"] = height
|
|
72
|
-
if full_page is not None:
|
|
73
|
-
params["full_page"] = str(full_page).lower()
|
|
74
|
-
if quality is not None:
|
|
75
|
-
params["quality"] = quality
|
|
76
|
-
if delay is not None:
|
|
77
|
-
params["delay"] = delay
|
|
78
|
-
if dark_mode is not None:
|
|
79
|
-
params["dark_mode"] = str(dark_mode).lower()
|
|
80
|
-
if block_ads is not None:
|
|
81
|
-
params["block_ads"] = str(block_ads).lower()
|
|
82
|
-
if block_cookie_banners is not None:
|
|
83
|
-
params["block_cookie_banners"] = str(block_cookie_banners).lower()
|
|
84
|
-
if hide_selectors is not None:
|
|
85
|
-
params["hide_selectors"] = hide_selectors
|
|
86
|
-
if click_selector is not None:
|
|
87
|
-
params["click_selector"] = click_selector
|
|
88
|
-
if device is not None:
|
|
89
|
-
params["device"] = device
|
|
90
|
-
if user_agent is not None:
|
|
91
|
-
params["user_agent"] = user_agent
|
|
92
|
-
if cache is not None:
|
|
93
|
-
params["cache"] = str(cache).lower()
|
|
94
|
-
if cache_ttl is not None:
|
|
95
|
-
params["cache_ttl"] = cache_ttl
|
|
96
|
-
if response_type is not None:
|
|
97
|
-
params["response_type"] = response_type
|
|
98
|
-
|
|
99
|
-
resp = self._client.get("/v1/screenshot", params=params)
|
|
100
|
-
if resp.status_code != 200:
|
|
101
|
-
self._raise_error(resp)
|
|
102
|
-
if response_type == "json":
|
|
103
|
-
return resp.json()
|
|
104
|
-
return resp.content
|
|
105
|
-
|
|
106
|
-
def info(self, url: str, **kwargs: Any) -> dict[str, Any]:
|
|
107
|
-
"""Get cache info for a URL without capturing.
|
|
108
|
-
|
|
109
|
-
Returns a dict with keys: url, cached, cache_key, cached_at,
|
|
110
|
-
expires_at, content_type.
|
|
111
|
-
"""
|
|
112
|
-
params: dict[str, Any] = {"url": url, **kwargs}
|
|
113
|
-
resp = self._client.get("/v1/screenshot/info", params=params)
|
|
114
|
-
if resp.status_code != 200:
|
|
115
|
-
self._raise_error(resp)
|
|
116
|
-
data = resp.json()
|
|
117
|
-
return {
|
|
118
|
-
"url": data["url"],
|
|
119
|
-
"cached": data["cached"],
|
|
120
|
-
"cache_key": data.get("cache_key"),
|
|
121
|
-
"cached_at": data.get("cached_at"),
|
|
122
|
-
"expires_at": data.get("expires_at"),
|
|
123
|
-
"content_type": data.get("content_type"),
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
def usage(self) -> dict[str, Any]:
|
|
127
|
-
"""Get current month's usage.
|
|
128
|
-
|
|
129
|
-
Returns a dict with keys: plan, used, limit, remaining, period.
|
|
130
|
-
"""
|
|
131
|
-
resp = self._client.get("/v1/usage")
|
|
132
|
-
if resp.status_code != 200:
|
|
133
|
-
self._raise_error(resp)
|
|
134
|
-
data = resp.json()
|
|
135
|
-
return {
|
|
136
|
-
"plan": data["plan"],
|
|
137
|
-
"used": data["usage"]["screenshots_used"],
|
|
138
|
-
"limit": data["usage"]["screenshots_limit"],
|
|
139
|
-
"remaining": data["usage"]["screenshots_remaining"],
|
|
140
|
-
"period": data["period"],
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
def usage_daily(self, days: int = 30) -> dict[str, Any]:
|
|
144
|
-
"""Get daily usage breakdown."""
|
|
145
|
-
resp = self._client.get("/v1/usage/daily", params={"days": days})
|
|
146
|
-
if resp.status_code != 200:
|
|
147
|
-
self._raise_error(resp)
|
|
148
|
-
return resp.json()
|
|
149
|
-
|
|
150
|
-
def close(self) -> None:
|
|
151
|
-
"""Close the HTTP client."""
|
|
152
|
-
self._client.close()
|
|
153
|
-
|
|
154
|
-
def __enter__(self) -> SnapRender:
|
|
155
|
-
return self
|
|
156
|
-
|
|
157
|
-
def __exit__(self, *args: Any) -> None:
|
|
158
|
-
self.close()
|
|
159
|
-
|
|
160
|
-
@staticmethod
|
|
161
|
-
def _raise_error(resp: httpx.Response) -> None:
|
|
162
|
-
try:
|
|
163
|
-
body = resp.json()
|
|
164
|
-
err = body.get("error", {})
|
|
165
|
-
raise SnapRenderError(
|
|
166
|
-
err.get("message", f"HTTP {resp.status_code}"),
|
|
167
|
-
err.get("code", "UNKNOWN"),
|
|
168
|
-
resp.status_code,
|
|
169
|
-
)
|
|
170
|
-
except (ValueError, KeyError):
|
|
171
|
-
raise SnapRenderError(f"HTTP {resp.status_code}", "UNKNOWN", resp.status_code)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|