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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: snaprender
3
- Version: 0.2.4
3
+ Version: 0.4.0
4
4
  Summary: Official Python SDK for SnapRender Screenshot API
5
5
  Project-URL: Homepage, https://snap-render.com
6
6
  Project-URL: Documentation, https://snap-render.com/docs
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "snaprender"
7
- version = "0.2.4"
7
+ version = "0.4.0"
8
8
  description = "Official Python SDK for SnapRender Screenshot API"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -3,4 +3,4 @@
3
3
  from .client import SnapRender, SnapRenderError
4
4
 
5
5
  __all__ = ["SnapRender", "SnapRenderError"]
6
- __version__ = "0.2.4"
6
+ __version__ = "0.4.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